package de.aaschmid.gradle.plugins.cpd;

import java.io.File;
import java.util.List;

import de.aaschmid.gradle.plugins.cpd.internal.CpdXmlFileReportImpl;
import de.aaschmid.gradle.plugins.cpd.test.GradleExtension;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.TaskProvider;
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 java.util.Arrays.asList;
import static java.util.Collections.singleton;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.params.provider.Arguments.of;

@ExtendWith(GradleExtension.class)
class CpdTest {

    @Test
    void Cpd_shouldAllowConfigurationOfDefaultTaskProperties(Project project, TaskProvider<Cpd> cpdCheck) {
        // Given:
        Task dependantTask = project.getTasks().create("dependant");

        // When:
        cpdCheck.configure(task -> {
            task.setDependsOn(singleton(dependantTask));
            task.setDescription("Execute me!");
            task.setEnabled(false);
            task.setExcludes(asList("*.kt", "*.txt"));
            task.setGroup("check");
        });

        // Then:
        Cpd actual = cpdCheck.get();
        assertThat(actual.getDependsOn()).containsOnly(dependantTask);
        assertThat(actual.getDescription()).isEqualTo("Execute me!");
        assertThat(actual.getEnabled()).isFalse();
        assertThat(actual.getExcludes()).containsOnly("*.kt", "*.txt");
        assertThat(actual.getGroup()).isEqualTo("check");
    }

    @Test
    void Cpd_shouldAllowConfigurationOfSourceTaskProperties(TaskProvider<Cpd> cpdCheck) {
        // Given:
        cpdCheck.configure(task -> {
            task.exclude("**/literal/*");
            task.exclude("**/*z*.java");
            task.include("**/*2.java");
            task.source(testFile(JAVA, "."));
        });
        Cpd actual = cpdCheck.get();

        // Expect:
        assertThat(actual.getExcludes()).containsOnly("**/literal/*", "**/*z*.java");
        assertThat(actual.getIncludes()).containsOnly("**/*2.java");
        assertThat(actual.getSource()).containsOnly(testFile(JAVA, "de/aaschmid/identifier/Identifier2.java"));
    }

    @Test
    void Cpd_shouldAllowConfigurationOfCpdTaskProperties(Project project, TaskProvider<Cpd> cpdCheck) {
        // Given:
        List<File> expectedPmdClasspath = createProjectFiles(project, "libs/pmd-classpath/");

        // When:
        cpdCheck.configure(task -> {
            task.setDescription("Execute me!");
            task.setGroup("check");

            task.setEncoding("ISO-8859-1");
            task.setIgnoreAnnotations(true);
            task.setIgnoreFailures(true);
            task.setIgnoreIdentifiers(true);
            task.setIgnoreLiterals(true);
            task.setLanguage("cpp");
            task.setMinimumTokenCount(10);
            task.setPmdClasspath(project.files(expectedPmdClasspath));
            task.setSkipDuplicateFiles(true);
            task.setSkipLexicalErrors(true);
            task.setSkipBlocks(false);
            task.setSkipBlocksPattern("<template|>");
        });

        // Then:
        Cpd actual = cpdCheck.get();
        assertThat(actual.getEncoding()).isEqualTo("ISO-8859-1");
        assertThat(actual.getIgnoreAnnotations()).isTrue();
        assertThat(actual.getIgnoreFailures()).isTrue();
        assertThat(actual.getIgnoreIdentifiers()).isTrue();
        assertThat(actual.getIgnoreLiterals()).isTrue();
        assertThat(actual.getLanguage()).isEqualTo("cpp");
        assertThat(actual.getMinimumTokenCount()).isEqualTo(10);
        assertThat(actual.getPmdClasspath()).containsExactlyInAnyOrderElementsOf(expectedPmdClasspath);
        assertThat(actual.getSkipDuplicateFiles()).isTrue();
        assertThat(actual.getSkipLexicalErrors()).isTrue();
        assertThat(actual.getSkipBlocks()).isFalse();
        assertThat(actual.getSkipBlocksPattern()).isEqualTo("<template|>");
    }

    @Test
    void Cpd_shouldAllowConfigurationOfCpdTaskReportProperties(Project project, TaskProvider<Cpd> cpdCheck) {
        // When:
        cpdCheck.configure(task -> task.reports(reports -> {
            reports.getCsv().setDestination(project.file(project.getBuildDir() + "/cpdCheck.csv"));
            reports.getCsv().setEnabled(true);
            reports.getCsv().setSeparator(';');
            reports.getCsv().setIncludeLineCount(false);

            reports.getText().setDestination(project.file(project.getBuildDir() + "/cpdCheck.text"));
            reports.getText().setEnabled(true);
            reports.getText().setLineSeparator("-_-");
            reports.getText().setTrimLeadingCommonSourceWhitespaces(true);

            reports.getVs().setDestination(project.file("cpdCheck.vs"));
            reports.getVs().setEnabled(true);

            reports.getXml().setDestination(project.file(project.getBuildDir() + "/reports/cpdCheck.xml"));
            reports.getXml().setEnabled(false);
            reports.getXml().setEncoding("UTF-16");
        }));

        // Then:
        CpdReports actual = cpdCheck.get().getReports();
        assertThat(actual.getCsv().getDestination()).isEqualTo(project.file("build/cpdCheck.csv"));
        assertThat(actual.getCsv().isEnabled()).isTrue();
        assertThat(actual.getCsv().getSeparator()).isEqualTo(';');
        assertThat(actual.getCsv().isIncludeLineCount()).isFalse();
        assertThat(actual.getText().getDestination()).isEqualTo(project.file("build/cpdCheck.text"));
        assertThat(actual.getText().isEnabled()).isTrue();
        assertThat(actual.getText().getLineSeparator()).isEqualTo("-_-");
        assertThat(actual.getText().getTrimLeadingCommonSourceWhitespaces()).isTrue();
        assertThat(actual.getVs().getDestination()).isEqualTo(project.file("cpdCheck.vs"));
        assertThat(actual.getVs().isEnabled()).isTrue();
        assertThat(actual.getXml().getDestination()).isEqualTo(project.file("build/reports/cpdCheck.xml"));
        assertThat(actual.getXml().isEnabled()).isFalse();
        assertThat(actual.getXml().getEncoding()).isEqualTo("UTF-16");
    }

    @Test
    void Cpd_shouldHaveCorrectTaskInputs(Project project, TaskProvider<Cpd> cpdCheck) {
        // Given:
        cpdCheck.configure(task -> {
            task.reports(report -> {
                report.getText().setDestination(project.file(project.getBuildDir() + "/cpdCheck.text"));
                report.getText().setEnabled(true);
            });
            task.source(testFile(JAVA, "de/aaschmid/clazz/"));
        });
        Cpd actual = cpdCheck.get();

        // Expect:
        assertThat(actual.getInputs().getProperties()).hasSize(50);
        assertThat(actual.getInputs().getSourceFiles()).containsExactlyInAnyOrderElementsOf(testFilesRecurseIn(JAVA, "de/aaschmid/clazz"));
    }

    @Test
    void Cpd_shouldHaveCorrectTaskOutputs(Project project, TaskProvider<Cpd> cpdCheck) {
        // Given:
        cpdCheck.configure(task -> {
            task.reports(report -> {
                report.getCsv().setDestination(project.file(project.getBuildDir() + "/cpd.csv"));
                report.getCsv().setEnabled(false);
                report.getText().setDestination(project.file("cpdCheck.txt"));
                report.getText().setEnabled(true);
                report.getVs().setDestination(project.file("cpd.vs"));
            });
            task.source(testFile(JAVA, "."));
        });
        Cpd actual = cpdCheck.get();

        // Expect:
        assertThat(actual.getOutputs().getFiles()).containsExactlyInAnyOrder(
                project.file(project.getBuildDir() + "/cpd.csv"),
                project.file("cpdCheck.txt"),
                project.file("cpd.vs"),
                project.file(project.getBuildDir() + "/reports/cpd/cpdCheck.xml")
        );
    }

    @Test
    void Cpd_shouldThrowInvalidUserDataExceptionIfEncodingIsNull(TaskProvider<Cpd> cpdCheck) {
        // Given:
        cpdCheck.configure(task -> task.setEncoding(null));
        Cpd actual = cpdCheck.get();

        // Expect:
        assertThatThrownBy(() -> actual.getActions().forEach(a -> a.execute(actual)))
                .isInstanceOf(InvalidUserDataException.class)
                .hasMessage("Task 'cpdCheck' requires 'encoding' but was: null.");
    }

    @Test
    void Cpd_shouldThrowInvalidUserDataExceptionIfMinimumTokenCountIsMinusOne(TaskProvider<Cpd> cpdCheck) {
        // Given:
        cpdCheck.configure(task -> task.setMinimumTokenCount(-1));
        Cpd actual = cpdCheck.get();

        // Expect:
        assertThatThrownBy(() -> actual.getActions().forEach(a -> a.execute(actual)))
                .isInstanceOf(InvalidUserDataException.class)
                .hasMessageMatching("Task 'cpdCheck' requires 'minimumTokenCount' to be greater than zero.");
    }

    @Test
    void Cpd_shouldThrowInvalidUserDataExceptionIfTwoReportsAreEnabled(TaskProvider<Cpd> cpdCheck) {
        // Given:
        cpdCheck.configure(task -> task.reports(report -> {
            report.getCsv().setEnabled(false);
            report.getText().setEnabled(false);
            report.getVs().setEnabled(false);
            report.getXml().setEnabled(false);
        }));
        Cpd actual = cpdCheck.get();

        // Expect:
        assertThatThrownBy(() -> actual.getActions().forEach(a -> a.execute(actual)))
                .isInstanceOf(InvalidUserDataException.class)
                .hasMessage("Task 'cpdCheck' requires at least one enabled report.");
    }

    static Arguments[] getXmlRendererEncoding() {
        return new Arguments[] {
                // @formatter:off
                of("ISO-8859-15",   "ISO-8859-1",   "ISO-8859-1"),
                of(null,            "ISO-8859-1",   "ISO-8859-1"),
                of("ISO-8859-15",   null,           "ISO-8859-15"),
                of("ISO-8859-15",   null,           "ISO-8859-15"),
                of(null,            null,           System.getProperty("file.encoding")),
                // @formatter:on
        };
    }

    @ParameterizedTest
    @MethodSource
    void getXmlRendererEncoding(String taskEncoding, String reportEncoding, String expected, TaskProvider<Cpd> cpdCheck) {
        // Given:
        cpdCheck.configure(task -> task.setEncoding(taskEncoding));

        CpdXmlFileReportImpl report = new CpdXmlFileReportImpl("xml", cpdCheck.get());
        report.setEncoding(reportEncoding);

        // Expect:
        assertThat(cpdCheck.get().getXmlRendererEncoding(report)).isEqualTo(expected);
    }
}