package de.aaschmid.gradle.plugins.cpd;

import java.io.File;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;

import de.aaschmid.gradle.plugins.cpd.test.GradleExtension;
import net.sourceforge.pmd.cpd.Tokenizer;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.Configuration.State;
import org.gradle.api.plugins.BasePlugin;
import org.gradle.api.plugins.GroovyPlugin;
import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginConvention;
import org.gradle.api.plugins.ReportingBasePlugin;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.language.base.plugins.LanguageBasePlugin;
import org.gradle.language.base.plugins.LifecycleBasePlugin;
import org.gradle.language.cpp.plugins.CppPlugin;
import org.gradle.testfixtures.ProjectBuilder;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import static de.aaschmid.gradle.plugins.cpd.test.TestFileResolver.Lang.JAVA;
import static de.aaschmid.gradle.plugins.cpd.test.TestFileResolver.createProjectFiles;
import static de.aaschmid.gradle.plugins.cpd.test.TestFileResolver.testFile;
import static de.aaschmid.gradle.plugins.cpd.test.TestFileResolver.testFilesRecurseIn;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.params.provider.Arguments.*;

@ExtendWith(GradleExtension.class)
class CpdPluginTest {

    @Test
    void CpdPlugin_shouldApplyRequiredReportingBasePlugin(Project project) {
        project.getPlugins().hasPlugin(ReportingBasePlugin.class);
    }

    @Test
    void CpdPlugin_shouldCreateAndConfigureCpdExtension(CpdExtension cpd) {
        assertThat(cpd.getEncoding()).isEqualTo(System.getProperty("file.encoding"));
        assertThat(cpd.isIgnoreAnnotations()).isFalse();
        assertThat(cpd.isIgnoreIdentifiers()).isFalse();
        assertThat(cpd.isIgnoreFailures()).isFalse();
        assertThat(cpd.isIgnoreLiterals()).isFalse();
        assertThat(cpd.getLanguage()).isEqualTo("java");
        assertThat(cpd.getMinimumTokenCount()).isEqualTo(50);
        assertThat(cpd.isSkipDuplicateFiles()).isFalse();
        assertThat(cpd.isSkipLexicalErrors()).isFalse();
        assertThat(cpd.isSkipBlocks()).isTrue();
        assertThat(cpd.getSkipBlocksPattern()).isEqualTo(Tokenizer.DEFAULT_SKIP_BLOCKS_PATTERN);
        assertThat(cpd.getToolVersion()).isEqualTo("6.14.0");
    }

    @Test
    void CpdPlugin_shouldCreateAndConfigureCpdConfiguration(Configuration cpdConfiguration) {
        assertThat(cpdConfiguration).isNotNull();
        assertThat(cpdConfiguration.getDependencies()).isEmpty();
        assertThat(cpdConfiguration.getDescription()).isEqualTo("The CPD libraries to be used for this project.");
        assertThat(cpdConfiguration.getExtendsFrom()).isEmpty();
        assertThat(cpdConfiguration.getState()).isEqualTo(State.UNRESOLVED);
        assertThat(cpdConfiguration.isTransitive()).isTrue();
        assertThat(cpdConfiguration.isVisible()).isFalse();
    }

    @Test
    void CpdPlugin_shouldCreateAndConfigureCpdCheckTaskWithCorrectDefaultValues(Project project, Configuration cpdConfiguration, TaskProvider<Cpd> cpdCheck) {
        Cpd t = cpdCheck.get();

        assertThat(t).isInstanceOf(Cpd.class);
        assertThat(t.getDescription()).isEqualTo("Run CPD analysis for all sources");
        assertThat(t.getGroup()).isNull();

        assertThat(t.getEncoding()).isEqualTo(System.getProperty("file.encoding"));
        assertThat(t.getIgnoreAnnotations()).isFalse();
        assertThat(t.getIgnoreFailures()).isFalse();
        assertThat(t.getIgnoreIdentifiers()).isFalse();
        assertThat(t.getIgnoreLiterals()).isFalse();
        assertThat(t.getLanguage()).isEqualTo("java");
        assertThat(t.getMinimumTokenCount()).isEqualTo(50);

        assertThat(t.getPmdClasspath()).isEqualTo(cpdConfiguration);

        assertThat(t.getReports().getCsv().getDestination()).isEqualTo(project.file("build/reports/cpd/cpdCheck.csv"));
        assertThat(t.getReports().getCsv().isEnabled()).isFalse();
        assertThat(t.getReports().getText().getDestination()).isEqualTo(project.file("build/reports/cpd/cpdCheck.text"));
        assertThat(t.getReports().getText().isEnabled()).isFalse();
        assertThat(t.getReports().getVs().getDestination()).isEqualTo(project.file("build/reports/cpd/cpdCheck.vs"));
        assertThat(t.getReports().getVs().isEnabled()).isFalse();
        assertThat(t.getReports().getXml().getDestination()).isEqualTo(project.file("build/reports/cpd/cpdCheck.xml"));
        assertThat(t.getReports().getXml().isEnabled()).isTrue();

        assertThat(t.getSkipDuplicateFiles()).isFalse();
        assertThat(t.getSkipLexicalErrors()).isFalse();
        assertThat(t.getSkipBlocks()).isTrue();
        assertThat(t.getSkipBlocksPattern()).isEqualTo(Tokenizer.DEFAULT_SKIP_BLOCKS_PATTERN);

        assertThat(t.getSource()).isEmpty();
    }

    @Test
    void CpdPlugin_shouldConfigureProperDefaultsForAdditionalCpdTask(Project project, Configuration cpdConfiguration) {
        Cpd t = project.getTasks().create("cpdCustom", Cpd.class);

        // expect:
        assertThat(t).isInstanceOf(Cpd.class);
        assertThat(t.getDescription()).isNull();
        assertThat(t.getGroup()).isNull();

        assertThat(t.getEncoding()).isEqualTo(System.getProperty("file.encoding"));
        assertThat(t.getIgnoreAnnotations()).isFalse();
        assertThat(t.getIgnoreFailures()).isFalse();
        assertThat(t.getIgnoreIdentifiers()).isFalse();
        assertThat(t.getIgnoreLiterals()).isFalse();
        assertThat(t.getLanguage()).isEqualTo("java");
        assertThat(t.getMinimumTokenCount()).isEqualTo(50);

        assertThat(t.getPmdClasspath()).isEqualTo(cpdConfiguration);

        assertThat(t.getReports().getCsv().getDestination()).isEqualTo(project.file("build/reports/cpd/cpdCustom.csv"));
        assertThat(t.getReports().getCsv().isEnabled()).isFalse();
        assertThat(t.getReports().getText().getDestination()).isEqualTo(project.file("build/reports/cpd/cpdCustom.text"));
        assertThat(t.getReports().getText().isEnabled()).isFalse();
        assertThat(t.getReports().getVs().getDestination()).isEqualTo(project.file("build/reports/cpd/cpdCustom.vs"));
        assertThat(t.getReports().getVs().isEnabled()).isFalse();
        assertThat(t.getReports().getXml().getDestination()).isEqualTo(project.file("build/reports/cpd/cpdCustom.xml"));
        assertThat(t.getReports().getXml().isEnabled()).isTrue();

        assertThat(t.getSkipDuplicateFiles()).isFalse();
        assertThat(t.getSkipLexicalErrors()).isFalse();
        assertThat(t.getSkipBlocks()).isTrue();
        assertThat(t.getSkipBlocksPattern()).isEqualTo(Tokenizer.DEFAULT_SKIP_BLOCKS_PATTERN);

        assertThat(t.getSource()).isEmpty();
    }

    static Stream<Class<? extends Plugin>> CpdPlugin_shouldAddCpdCheckTaskAsDependencyOfCheckLifecycleTaskIfPluginIsApplied() {
        return Stream.of(
                LifecycleBasePlugin.class,
                BasePlugin.class,
                LanguageBasePlugin.class,
                JavaBasePlugin.class,

                JavaPlugin.class,
                GroovyPlugin.class,
                CppPlugin.class
            );
    }

    @ParameterizedTest
    @MethodSource
    void CpdPlugin_shouldAddCpdCheckTaskAsDependencyOfCheckLifecycleTaskIfPluginIsApplied(Class<? extends Plugin> pluginClass, Project project, TaskProvider<Cpd> cpdCheck) {
        // When:
        project.getPlugins().apply(pluginClass);

        // Then:
        Task check = project.getTasks().getByName("check");
        @SuppressWarnings("unchecked") Set<Task> dependencies = (Set<Task>) check.getTaskDependencies().getDependencies(check);

        assertThat(check.getDependsOn()).contains(cpdCheck);
        assertThat(dependencies).contains(cpdCheck.get());
    }

    @Test
    void CpdPlugin_shouldAddCpdCheckTaskAsDependencyOfCheckLifecycleTaskIfJavaPluginIsApplied(Project project, TaskProvider<Cpd> cpdCheck) {
        // When:
        project.getPlugins().apply(JavaBasePlugin.class);

        project.getConvention().getPlugin(JavaPluginConvention.class).getSourceSets().create("tmp", (SourceSet sourceSet) ->
                sourceSet.getJava().srcDir(testFile(JAVA, ".")));

        Task checkTask = project.getTasks().getByName("check");

        // Then:
        assertThat((Set<Task>) checkTask.getTaskDependencies().getDependencies(checkTask)).contains(cpdCheck.get());
        assertThat(cpdCheck.get().getSource()).containsExactlyInAnyOrderElementsOf(testFilesRecurseIn(JAVA, "."));
    }

    @Test
    void CpdPlugin_shouldSetCpdCheckSourceEqualsToMainAndTestSourceSetsIfJavaPluginIsApplied(Project project, TaskProvider<Cpd> cpd) {
        // Given:
        String mainFile = "src/main/java/Clazz.java";
        String testFile = "src/test/java/ClazzTest.java";

        // When:
        project.getPlugins().apply(JavaPlugin.class);
        createProjectFiles(project, mainFile, "src/resources/java/message.properties", testFile);

        project.getConvention().getPlugin(JavaPluginConvention.class).getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME, sourceSet -> {
            sourceSet.getJava().srcDir(testFile(JAVA, "de/aaschmid/annotation"));
            sourceSet.getAllJava().srcDir(testFile(JAVA, "de/aaschmid/clazz"));
            sourceSet.getResources().srcDir(testFile(JAVA, "de/aaschmid/foo"));
        });
        project.getConvention().getPlugin(JavaPluginConvention.class).getSourceSets().getByName(SourceSet.TEST_SOURCE_SET_NAME, sourceSet ->
                sourceSet.getJava().srcDir(testFile(JAVA, "de/aaschmid/test")));

        // Then:
        List<File> expected = testFilesRecurseIn(JAVA, "de/aaschmid/annotation", "de/aaschmid/clazz", "de/aaschmid/test");
        expected.add(project.file(mainFile));
        expected.add(project.file(testFile));

        assertThat(cpd.get().getSource()).containsExactlyInAnyOrderElementsOf(expected);
    }

    @Test
    void CpdPlugin_shouldAddSourcesOfSubProjectsEvenIfAppliedOnlyOnParentProject(Project project, TaskProvider<Cpd> cpdCheck) {
        // When:
        Project subProject1 = ProjectBuilder.builder().withName("sub1").withParent(project).build();
        subProject1.getPlugins().apply(JavaPlugin.class);
        createProjectFiles(subProject1, "src/main/java/Clazz.java", "src/test/java/ClazzTest.java");

        Project subProject2 = ProjectBuilder.builder().withName("sub2").withParent(project).build();
        subProject2.getPlugins().apply(GroovyPlugin.class);
        createProjectFiles(subProject2, "src/main/groovy/Clazz.groovy", "src/main/resources/clazz.properties");

        subProject1.getConvention().getPlugin(JavaPluginConvention.class).getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME, sourceSet -> {
            sourceSet.getJava().srcDir(testFile(JAVA, "de/aaschmid/annotation"));
            sourceSet.getAllJava().srcDir(testFile(JAVA, "de/aaschmid/clazz"));
            sourceSet.getResources().srcDir(testFile(JAVA, "de/aaschmid/foo"));
        });
        subProject2.getConvention().getPlugin(JavaPluginConvention.class).getSourceSets().getByName(SourceSet.TEST_SOURCE_SET_NAME, sourceSet ->
                sourceSet.getJava().srcDir(testFile(JAVA, "de/aaschmid/test")));

        // Then:
        List<File> expected = testFilesRecurseIn(JAVA, "de/aaschmid/annotation", "de/aaschmid/clazz", "de/aaschmid/test");
        expected.add(subProject1.file("src/main/java/Clazz.java"));
        expected.add(subProject1.file("src/test/java/ClazzTest.java"));
        expected.add(subProject2.file("src/main/groovy/Clazz.groovy"));

        assertThat(cpdCheck.get().getSource()).containsExactlyInAnyOrderElementsOf(expected);

        assertThat(subProject1.getTasks().findByName("cpdCheck")).isNull();
        assertThat(subProject2.getTasks().findByName("cpdCheck")).isNull();
    }

    @Test
    void CpdPlugin_shouldAllowConfigureToolDependenciesExplicitlyViaToolVersionProperty(Project project, Configuration cpdConfiguration, CpdExtension cpd, TaskProvider<Cpd> cpdCheck) {
        // Given:
        project.getRepositories().mavenLocal();
        project.getRepositories().mavenCentral();

        // When:
        cpd.setToolVersion("5.2.1");

        // Then:
        assertThat(cpdCheck.get().getPmdClasspath()).isEqualTo(cpdConfiguration);
        assertThat(cpdConfiguration.resolve()).anyMatch(file -> file.getName().equals("pmd-core-5.2.1.jar"));
    }

    @Test
    void CpdPlugin_shouldAllowConfigureToolDependenciesExplicitlyViaConfiguration(Project project, Configuration cpdConfiguration, TaskProvider<Cpd> cpdCheck) {
        // Given:
        project.getRepositories().mavenLocal();
        project.getRepositories().mavenCentral();

        // When:
        project.getDependencies().add("cpd", "net.sourceforge.pmd:pmd:5.0.2");

        // Then:
        assertThat(cpdCheck.get().getPmdClasspath()).isEqualTo(cpdConfiguration);
        assertThat(cpdConfiguration.resolve()).anyMatch(file -> file.getName().equals("pmd-5.0.2.jar"));
    }

    @Test
    void CpdPlugin_shouldAllowConfigureCpdCheckTaskViaCpdExtension(Project project, CpdExtension cpd, TaskProvider<Cpd> cpdCheck) {
        // Given:
        cpd.setEncoding("UTF-8");
        cpd.setIgnoreAnnotations(true);
        cpd.setIgnoreFailures(true);
        cpd.setIgnoreIdentifiers(true);
        cpd.setIgnoreLiterals(true);
        cpd.setLanguage("ruby");
        cpd.setMinimumTokenCount(25);
        cpd.setReportsDir(project.file("cpd-reports"));
        cpd.setSkipDuplicateFiles(true);
        cpd.setSkipLexicalErrors(true);
        cpd.setSkipBlocks(false);
        cpd.setSkipBlocksPattern("<|>");

        // When:
        Cpd task = cpdCheck.get();

        // Then:
        assertThat(task.getEncoding()).isEqualTo("UTF-8");
        assertThat(task.getIgnoreAnnotations()).isTrue();
        assertThat(task.getIgnoreFailures()).isTrue();
        assertThat(task.getIgnoreIdentifiers()).isTrue();
        assertThat(task.getIgnoreLiterals()).isTrue();
        assertThat(task.getLanguage()).isEqualTo("ruby");
        assertThat(task.getMinimumTokenCount()).isEqualTo(25);
        assertThat(task.getReports().getCsv().getDestination()).isEqualTo(project.file("cpd-reports/cpdCheck.csv"));
        assertThat(task.getReports().getText().getDestination()).isEqualTo(project.file("cpd-reports/cpdCheck.text"));
        assertThat(task.getReports().getVs().getDestination()).isEqualTo(project.file("cpd-reports/cpdCheck.vs"));
        assertThat(task.getReports().getXml().getDestination()).isEqualTo(project.file("cpd-reports/cpdCheck.xml"));
        assertThat(task.getSkipDuplicateFiles()).isTrue();
        assertThat(task.getSkipLexicalErrors()).isTrue();
        assertThat(task.getSkipBlocks()).isFalse();
        assertThat(task.getSkipBlocksPattern()).isEqualTo("<|>");
    }

    @Test
    void CpdPlugin_shouldAllowConfigureAdditionalCpdTaskViaExtension(Project project, CpdExtension cpd) {
        // Given:
        cpd.setLanguage("php");
        cpd.setMinimumTokenCount(250);

        // When:
        Cpd task = project.getTasks().create("cpdCustom", Cpd.class);

        // Then:
        assertThat(task.getLanguage()).isEqualTo("php");
        assertThat(task.getMinimumTokenCount()).isEqualTo(250);
    }
}