/*
 * Copyright 2023 the original author or authors.
 *
 * 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.gradle.internal.declarativedsl.project

import org.gradle.api.internal.plugins.BindsProjectType
import org.gradle.api.internal.plugins.ProjectTypeBinding
import org.gradle.api.internal.plugins.ProjectTypeBindingBuilder
import org.gradle.api.internal.plugins.software.RegistersSoftwareTypes
import org.gradle.integtests.fixtures.AbstractIntegrationSpec
import org.intellij.lang.annotations.Language

class DeclarativeDslProjectBuildFileIntegrationSpec extends AbstractIntegrationSpec {

    def 'schema is written during project files interpretation with declarative DSL'() {
        given:
        settingsFile("""
            include(":sub")
        """)

        file("build.gradle.dcl") << ""

        file("sub/build.gradle.dcl") << ""

        when:
        run(":help")

        then:
        [
            file(".gradle/declarative-schema/project.dcl.schema"),
        ].every { it.isFile() && it.text != "" }
    }

    def 'can configure a custom plugin extension in declarative DSL for a plugin written in #language'() {
        given:
        simpleDeclarativePlugin(language)

        file("build.gradle.dcl") << """
            restricted {
                id = "test"

                referencePoint = point(1, 2)
                arguments = listOf("one", "two")
                flags = listOf()

                primaryAccess {
                    read = false
                    write = false
                }

                secondaryAccess {
                    name = "two"
                    read = true
                    write = false
                }

                secondaryAccess {
                    name = "three"
                    read = true
                    write = true
                }
            }
        """

        when:
        run(":printConfiguration")

        then:
        outputContains("""id = test
referencePoint = (1, 2)
arguments = [one, two]
flags = []
mapProperty = {}
primaryAccess = { primary, false, false}
secondaryAccess { two, true, false}
secondaryAccess { three, true, true}"""
        )

        where:
        language | _
        "java"   | _
        "kotlin" | _
    }

    def 'can use list augmentation with += from a DCL file'() {
        given:
        simpleDeclarativePlugin(language)

        file("settings.gradle.dcl") << """

            defaults {
                restricted {
                    arguments = listOf("one")
                    flags = listOf(1, 2)
                }
            }
        """

        file("build.gradle.dcl") << """
            restricted {
                arguments += listOf("two", "three")
                flags += listOf(3)
            }
        """

        when:
        run(":printConfiguration", "--stacktrace")

        then:
        outputContains("arguments = [one, two, three]")
        outputContains("flags = [1, 2, 3]")

        where:
        language | _
        "java"   | _
        "kotlin" | _
    }

    def 'can set and augment map properties from a DCL file'() {
        given:
        simpleDeclarativePlugin(language)

        file("settings.gradle.dcl") << """

            defaults {
                restricted {
                    mapProperty = mapOf("one" to 1, "two" to 2)
                }
            }
        """

        file("build.gradle.dcl") << """
            restricted {
                mapProperty += mapOf("three" to 3, "four" to 4)
            }
        """

        when:
        run(":printConfiguration")

        then:
        outputContains("mapProperty = {one=1, two=2, three=3, four=4}")

        where:
        language | _
        "java"   | _
        "kotlin" | _
    }


    def simpleDeclarativePlugin(String language = "kotlin") {
        file("build-logic/build.gradle.kts") << defineCustomPluginBuild(language)
        file("build-logic/src/main/java/com/example/restricted/SoftwareTypeRegistrationPlugin.java") << defineSettingsPluginRegisteringSoftwareTypeProvidingPlugin()
        file("build-logic/src/main/java/com/example/restricted/RestrictedPlugin.java") << defineRestrictedPlugin()

        if (language == "kotlin") {
            file(KOTLIN_PLUGIN_EXTENSION_FILENAME) << KOTLIN_PLUGIN_EXTENSION
        } else if (language == "java") {
            file(JAVA_PLUGIN_EXTENSION_FILENAME) << JAVA_PLUGIN_EXTENSION
        }

        file("settings.gradle.dcl") << """
            pluginManagement {
                includeBuild("build-logic")
            }
            plugins {
                id("com.example.restricted.ecosystem")
            }
        """

    }

    private static String defineRestrictedPlugin() {
        """
            package com.example.restricted;

            import org.gradle.api.DefaultTask;
            import org.gradle.api.Plugin;
            import org.gradle.api.Project;
            import org.gradle.api.provider.ListProperty;
            import org.gradle.api.provider.Property;
            import ${BindsProjectType.class.name};
            import ${ProjectTypeBinding.class.name};
            import ${ProjectTypeBindingBuilder.class.name};

            @${BindsProjectType.class.simpleName}(RestrictedPlugin.Binding.class)
            public abstract class RestrictedPlugin implements Plugin<Project> {
                static class Binding implements ${ProjectTypeBinding.class.simpleName} {
                    public void bind(${ProjectTypeBindingBuilder.class.simpleName} builder) {
                        builder.bindProjectType("restricted",  Extension.class, (context, definition, model) -> {
                            context.getProject().getTasks().register("printConfiguration", DefaultTask.class, task -> {
                                Property<Extension.Point> referencePoint = definition.getReferencePoint();
                                Extension.Access acc = definition.getPrimaryAccess();
                                ListProperty<Extension.Access> secondaryAccess = definition.getSecondaryAccess();

                                task.doLast("print restricted extension content", t -> {
                                    System.out.println("id = " + definition.getId().get());
                                    Extension.Point point = referencePoint.getOrElse(definition.point(-1, -1));
                                    System.out.println("referencePoint = (" + point.getX() + ", " + point.getY() + ")");
                                    System.out.println("arguments = " + definition.getArguments().get());
                                    System.out.println("flags = " + definition.getFlags());
                                    System.out.println("mapProperty = " + definition.getMapProperty().get());
                                    System.out.println("primaryAccess = { " +
                                            acc.getName().get() + ", " + acc.getRead().get() + ", " + acc.getWrite().get() + "}"
                                    );
                                    secondaryAccess.get().forEach(it -> {
                                        System.out.println("secondaryAccess { " +
                                                it.getName().get() + ", " + it.getRead().get() + ", " + it.getWrite().get() +
                                                "}"
                                        );
                                    });
                                });
                            });
                        })
                        .withUnsafeDefinition();
                    }
                }

                @Override
                public void apply(Project target) {

                }
            }
        """
    }

    private static String defineCustomPluginBuild(String language) {
        """
            plugins {
                `java-gradle-plugin`
                ${if (language == "kotlin") { "`kotlin-dsl`" } else { "" }}
            }
            ${if (language == "kotlin") { "repositories { mavenCentral() }" } else { "" }}
            gradlePlugin {
                plugins {
                    create("restrictedPlugin") {
                        id = "com.example.restricted"
                        implementationClass = "com.example.restricted.RestrictedPlugin"
                    }
                    create("softwareTypeRegistrator") {
                        id = "com.example.restricted.ecosystem"
                        implementationClass = "com.example.restricted.SoftwareTypeRegistrationPlugin"
                    }
                }
            }
        """
    }

    private static final JAVA_PLUGIN_EXTENSION_FILENAME = "build-logic/src/main/java/com/example/restricted/Extension.java"
    private static final KOTLIN_PLUGIN_EXTENSION_FILENAME = "build-logic/src/main/kotlin/com/example/restricted/Extension.kt"

    @Language("java")
    private static final JAVA_PLUGIN_EXTENSION = """
        package com.example.restricted;

        import java.util.ArrayList;
        import java.util.List;
        import org.gradle.api.internal.plugins.Definition;import org.gradle.declarative.dsl.model.annotations.Adding;
        import org.gradle.declarative.dsl.model.annotations.HiddenInDefinition;
        import org.gradle.api.Action;
        import org.gradle.api.model.ObjectFactory;
        import org.gradle.api.provider.ListProperty;
        import org.gradle.api.provider.MapProperty;
        import org.gradle.api.provider.Property;
        import org.gradle.api.internal.plugins.Definition;
        import org.gradle.api.internal.plugins.BuildModel;

        import javax.inject.Inject;

        public abstract class Extension implements Definition<Extension.Model>{
            private final Access primaryAccess;
            public abstract ListProperty<Access> getSecondaryAccess();
            private final ObjectFactory objects;

            public Access getPrimaryAccess() {
                return primaryAccess;
            }

            @Inject
            public Extension(ObjectFactory objects) {
                this.objects = objects;
                this.primaryAccess = objects.newInstance(Access.class);
                this.primaryAccess.getName().set("primary");

                getId().convention("<no id>");
                getReferencePoint().convention(point(-1, -1));
            }

            public abstract Property<String> getId();

            public abstract Property<Point> getReferencePoint();

            public abstract ListProperty<String> getArguments();

            public abstract MapProperty<String, Integer> getMapProperty();

            private List<Integer> flags = new ArrayList<>();

            public List<Integer> getFlags() {
                return flags;
            }

            public void setFlags(List<Integer> flags) {
                this.flags = flags;
            }

            @Adding
            public Access secondaryAccess(Action<? super Access> configure) {
                Access newAccess = objects.newInstance(Access.class);
                newAccess.getName().convention("<no name>");
                configure.execute(newAccess);
                getSecondaryAccess().add(newAccess);
                return newAccess;
            }

            public Point point(int x, int y) {
                return new Point(x, y);
            }

            public abstract static class Access {
                public Access() {
                    getName().convention("<no name>");
                    getRead().convention(false);
                    getWrite().convention(false);
                }

                public abstract Property<String> getName();

                public abstract Property<Boolean> getRead();

                public abstract Property<Boolean> getWrite();
            }

            public static class Point {
                public final int xCoord;
                public final int yCoord;

                public Point(int x, int y) {
                    this.xCoord = x;
                    this.yCoord = y;
                }

                public int getX() {
                    return xCoord;
                }

                public int getY() {
                    return yCoord;
                }
            }

            interface Model extends BuildModel { }
        }
    """.stripIndent()

    @Language("kotlin")
    private static final KOTLIN_PLUGIN_EXTENSION = """
        package com.example.restricted

        import org.gradle.api.model.ObjectFactory
        import org.gradle.api.provider.ListProperty
        import org.gradle.api.provider.MapProperty
        import org.gradle.api.provider.Property
        import org.gradle.declarative.dsl.model.annotations.Adding
        import javax.inject.Inject
        import org.gradle.api.internal.plugins.Definition
        import org.gradle.api.internal.plugins.BuildModel

        abstract class Extension @Inject constructor(private val objects: ObjectFactory) : Definition<Extension.Model> {
            val primaryAccess: Access
            abstract val secondaryAccess: ListProperty<Access>

            init {
                this.primaryAccess = objects.newInstance(Access::class.java)
                primaryAccess.name.set("primary")

                id.convention("<no id>")
                referencePoint.convention(point(-1, -1))
            }

            abstract val id: Property<String>

            abstract val referencePoint: Property<Point>

            abstract val arguments: ListProperty<String>

            abstract val mapProperty: MapProperty<String, Int>

            var flags: List<Int> = emptyList()

            @Adding
            fun secondaryAccess(configure: Access.() -> Unit): Access {
                val newAccess = objects.newInstance(Access::class.java)
                newAccess.name.convention("<no name>")
                configure(newAccess)
                secondaryAccess.add(newAccess)
                return newAccess
            }

            fun point(x: Int, y: Int): Point {
                return Point(x, y)
            }

            abstract class Access {
                init {
                    name.convention("<no name>")
                    read.convention(false)
                    write.convention(false)
                }

                abstract val name: Property<String>

                abstract val read: Property<Boolean>

                abstract val write: Property<Boolean>
            }

            class Point(val x: Int, val y: Int)

            interface Model : BuildModel
        }
    """

    def 'reports #kind errors in project file'() {
        given:
        file("build.gradle.dcl") << bodyCode

        when:
        def failure = fails(":help")

        then:
        failure.assertHasErrorOutput(expectedMessage)

        where:
        kind               | bodyCode              | expectedMessage
        "syntax"           | "..."                 | "1:1: parsing error: Expecting an element"
        "language feature" | "@A dependencies { }" | "1:1: unsupported language feature: AnnotationUsage"
        "semantic"         | "x = 1"               | "1:1: unresolved reference 'x'"
    }

    private String defineSettingsPluginRegisteringSoftwareTypeProvidingPlugin() {
        return """
        package com.example.restricted;

        import org.gradle.api.DefaultTask;
        import org.gradle.api.Plugin;
        import org.gradle.api.initialization.Settings;
        import org.gradle.api.internal.SettingsInternal;
        import ${RegistersSoftwareTypes.class.name};

        @RegistersSoftwareTypes({ RestrictedPlugin.class })
        abstract public class SoftwareTypeRegistrationPlugin implements Plugin<Settings> {
            @Override
            public void apply(Settings target) {
            }
        }
        """
    }
}
