ToolchainPluginStrategyTest.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.maven.cling.invoker.mvnup.goals;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;

import eu.maveniverse.domtrip.Document;
import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * Unit tests for the {@link ToolchainPluginStrategy} class.
 */
@DisplayName("ToolchainPluginStrategy")
class ToolchainPluginStrategyTest {

    private static final Path POM_PATH = Paths.get("/project/pom.xml");

    @Nested
    @DisplayName("Applicability")
    class ApplicabilityTests {

        @Test
        @DisplayName("should be applicable when --all option is true")
        void applicableWithAll() {
            ToolchainPluginStrategy strategy = new ToolchainPluginStrategy();
            assertTrue(strategy.isApplicable(TestUtils.createMockContext(TestUtils.createOptionsWithAll(true))));
        }

        @Test
        @DisplayName("should be applicable with default options (no flags)")
        void applicableWithDefaults() {
            ToolchainPluginStrategy strategy = new ToolchainPluginStrategy();
            assertTrue(strategy.isApplicable(TestUtils.createMockContext()));
        }

        @Test
        @DisplayName("should be applicable when --model is true")
        void applicableWithModel() {
            ToolchainPluginStrategy strategy = new ToolchainPluginStrategy();
            assertTrue(strategy.isApplicable(TestUtils.createMockContext(TestUtils.createOptionsWithFixModel(true))));
        }

        @Test
        @DisplayName("should not be applicable when --model is false")
        void notApplicableWithModelFalse() {
            ToolchainPluginStrategy strategy = new ToolchainPluginStrategy();
            assertFalse(strategy.isApplicable(TestUtils.createMockContext(TestUtils.createOptionsWithFixModel(false))));
        }
    }

    @Nested
    @DisplayName("Source level detection")
    class SourceLevelDetectionTests {

        private ToolchainPluginStrategy strategy;

        @BeforeEach
        void setUp() {
            strategy = new ToolchainPluginStrategy();
        }

        @Test
        @DisplayName("should detect source level from maven.compiler.release property")
        void detectFromRelease() {
            String pomXml = """
                    <?xml version="1.0" encoding="UTF-8"?>
                    <project xmlns="http://maven.apache.org/POM/4.0.0">
                        <modelVersion>4.0.0</modelVersion>
                        <groupId>com.example</groupId>
                        <artifactId>test</artifactId>
                        <version>1.0</version>
                        <properties>
                            <maven.compiler.release>6</maven.compiler.release>
                        </properties>
                    </project>
                    """;
            Document doc = Document.of(pomXml);
            assertEquals(6, strategy.detectSourceLevel(doc));
        }

        @Test
        @DisplayName("should detect source level from maven.compiler.source property")
        void detectFromSource() {
            String pomXml = """
                    <?xml version="1.0" encoding="UTF-8"?>
                    <project xmlns="http://maven.apache.org/POM/4.0.0">
                        <modelVersion>4.0.0</modelVersion>
                        <groupId>com.example</groupId>
                        <artifactId>test</artifactId>
                        <version>1.0</version>
                        <properties>
                            <maven.compiler.source>1.6</maven.compiler.source>
                        </properties>
                    </project>
                    """;
            Document doc = Document.of(pomXml);
            assertEquals(6, strategy.detectSourceLevel(doc));
        }

        @Test
        @DisplayName("release property takes precedence over source property")
        void releaseTakesPrecedenceOverSource() {
            String pomXml = """
                    <?xml version="1.0" encoding="UTF-8"?>
                    <project xmlns="http://maven.apache.org/POM/4.0.0">
                        <modelVersion>4.0.0</modelVersion>
                        <groupId>com.example</groupId>
                        <artifactId>test</artifactId>
                        <version>1.0</version>
                        <properties>
                            <maven.compiler.release>7</maven.compiler.release>
                            <maven.compiler.source>6</maven.compiler.source>
                        </properties>
                    </project>
                    """;
            Document doc = Document.of(pomXml);
            assertEquals(7, strategy.detectSourceLevel(doc));
        }

        @Test
        @DisplayName("should detect from compiler plugin <release>")
        void detectFromCompilerPluginRelease() {
            String pomXml = """
                    <?xml version="1.0" encoding="UTF-8"?>
                    <project xmlns="http://maven.apache.org/POM/4.0.0">
                        <modelVersion>4.0.0</modelVersion>
                        <groupId>com.example</groupId>
                        <artifactId>test</artifactId>
                        <version>1.0</version>
                        <build>
                            <plugins>
                                <plugin>
                                    <artifactId>maven-compiler-plugin</artifactId>
                                    <configuration>
                                        <release>6</release>
                                    </configuration>
                                </plugin>
                            </plugins>
                        </build>
                    </project>
                    """;
            Document doc = Document.of(pomXml);
            assertEquals(6, strategy.detectSourceLevel(doc));
        }

        @Test
        @DisplayName("should detect from compiler plugin <source>")
        void detectFromCompilerPluginSource() {
            String pomXml = """
                    <?xml version="1.0" encoding="UTF-8"?>
                    <project xmlns="http://maven.apache.org/POM/4.0.0">
                        <modelVersion>4.0.0</modelVersion>
                        <groupId>com.example</groupId>
                        <artifactId>test</artifactId>
                        <version>1.0</version>
                        <build>
                            <plugins>
                                <plugin>
                                    <artifactId>maven-compiler-plugin</artifactId>
                                    <configuration>
                                        <source>1.5</source>
                                    </configuration>
                                </plugin>
                            </plugins>
                        </build>
                    </project>
                    """;
            Document doc = Document.of(pomXml);
            assertEquals(5, strategy.detectSourceLevel(doc));
        }

        @Test
        @DisplayName("should return -1 when no source level configured")
        void noSourceLevel() {
            String pomXml = """
                    <?xml version="1.0" encoding="UTF-8"?>
                    <project xmlns="http://maven.apache.org/POM/4.0.0">
                        <modelVersion>4.0.0</modelVersion>
                        <groupId>com.example</groupId>
                        <artifactId>test</artifactId>
                        <version>1.0</version>
                    </project>
                    """;
            Document doc = Document.of(pomXml);
            assertEquals(-1, strategy.detectSourceLevel(doc));
        }

        @Test
        @DisplayName("should detect from pluginManagement")
        void detectFromPluginManagement() {
            String pomXml = """
                    <?xml version="1.0" encoding="UTF-8"?>
                    <project xmlns="http://maven.apache.org/POM/4.0.0">
                        <modelVersion>4.0.0</modelVersion>
                        <groupId>com.example</groupId>
                        <artifactId>test</artifactId>
                        <version>1.0</version>
                        <build>
                            <pluginManagement>
                                <plugins>
                                    <plugin>
                                        <artifactId>maven-compiler-plugin</artifactId>
                                        <configuration>
                                            <release>6</release>
                                        </configuration>
                                    </plugin>
                                </plugins>
                            </pluginManagement>
                        </build>
                    </project>
                    """;
            Document doc = Document.of(pomXml);
            assertEquals(6, strategy.detectSourceLevel(doc));
        }
    }

    @Nested
    @DisplayName("Toolchains plugin detection")
    class ToolchainsPluginDetectionTests {

        private ToolchainPluginStrategy strategy;

        @BeforeEach
        void setUp() {
            strategy = new ToolchainPluginStrategy();
        }

        @Test
        @DisplayName("should detect existing select-jdk-toolchain goal")
        void detectExistingGoal() {
            String pomXml = """
                    <?xml version="1.0" encoding="UTF-8"?>
                    <project xmlns="http://maven.apache.org/POM/4.0.0">
                        <modelVersion>4.0.0</modelVersion>
                        <groupId>com.example</groupId>
                        <artifactId>test</artifactId>
                        <version>1.0</version>
                        <build>
                            <plugins>
                                <plugin>
                                    <groupId>org.apache.maven.plugins</groupId>
                                    <artifactId>maven-toolchains-plugin</artifactId>
                                    <executions>
                                        <execution>
                                            <goals>
                                                <goal>select-jdk-toolchain</goal>
                                            </goals>
                                        </execution>
                                    </executions>
                                </plugin>
                            </plugins>
                        </build>
                    </project>
                    """;
            Document doc = Document.of(pomXml);
            assertTrue(strategy.hasToolchainsPluginWithSelectGoal(doc));
        }

        @Test
        @DisplayName("should not detect toolchains plugin without select-jdk-toolchain goal")
        void noSelectGoal() {
            String pomXml = """
                    <?xml version="1.0" encoding="UTF-8"?>
                    <project xmlns="http://maven.apache.org/POM/4.0.0">
                        <modelVersion>4.0.0</modelVersion>
                        <groupId>com.example</groupId>
                        <artifactId>test</artifactId>
                        <version>1.0</version>
                        <build>
                            <plugins>
                                <plugin>
                                    <groupId>org.apache.maven.plugins</groupId>
                                    <artifactId>maven-toolchains-plugin</artifactId>
                                    <executions>
                                        <execution>
                                            <goals>
                                                <goal>toolchain</goal>
                                            </goals>
                                        </execution>
                                    </executions>
                                </plugin>
                            </plugins>
                        </build>
                    </project>
                    """;
            Document doc = Document.of(pomXml);
            assertFalse(strategy.hasToolchainsPluginWithSelectGoal(doc));
        }

        @Test
        @DisplayName("should not detect when no toolchains plugin present")
        void noToolchainsPlugin() {
            String pomXml = """
                    <?xml version="1.0" encoding="UTF-8"?>
                    <project xmlns="http://maven.apache.org/POM/4.0.0">
                        <modelVersion>4.0.0</modelVersion>
                        <groupId>com.example</groupId>
                        <artifactId>test</artifactId>
                        <version>1.0</version>
                    </project>
                    """;
            Document doc = Document.of(pomXml);
            assertFalse(strategy.hasToolchainsPluginWithSelectGoal(doc));
        }
    }

    @Nested
    @DisplayName("Plugin addition")
    class PluginAdditionTests {

        private ToolchainPluginStrategy strategy;

        @BeforeEach
        void setUp() {
            strategy = new ToolchainPluginStrategy();
        }

        @Test
        @DisplayName("should add toolchains plugin with version constraint to POM without build section")
        void addToEmptyPom() {
            String pomXml = """
                    <?xml version="1.0" encoding="UTF-8"?>
                    <project xmlns="http://maven.apache.org/POM/4.0.0">
                        <modelVersion>4.0.0</modelVersion>
                        <groupId>com.example</groupId>
                        <artifactId>test</artifactId>
                        <version>1.0</version>
                    </project>
                    """;
            Document doc = Document.of(pomXml);
            strategy.addToolchainsPlugin(doc, 11);

            assertTrue(strategy.hasToolchainsPluginWithSelectGoal(doc));
            String output = doc.toXml();
            assertTrue(output.contains("<version>(,11]</version>"), "Expected version constraint in output: " + output);
        }

        @Test
        @DisplayName("should add toolchains plugin with version constraint to POM with existing build section")
        void addToExistingBuild() {
            String pomXml = """
                    <?xml version="1.0" encoding="UTF-8"?>
                    <project xmlns="http://maven.apache.org/POM/4.0.0">
                        <modelVersion>4.0.0</modelVersion>
                        <groupId>com.example</groupId>
                        <artifactId>test</artifactId>
                        <version>1.0</version>
                        <build>
                            <plugins>
                                <plugin>
                                    <artifactId>maven-compiler-plugin</artifactId>
                                </plugin>
                            </plugins>
                        </build>
                    </project>
                    """;
            Document doc = Document.of(pomXml);
            strategy.addToolchainsPlugin(doc, 8);

            assertTrue(strategy.hasToolchainsPluginWithSelectGoal(doc));
            String output = doc.toXml();
            assertTrue(output.contains("<version>(,8]</version>"), "Expected version constraint in output: " + output);
        }
    }

    @Nested
    @DisplayName("Full apply")
    class ApplyTests {

        @Test
        @DisplayName("should add plugin when source level is incompatible with running JDK")
        void addsPluginWhenIncompatible() {
            // Simulate running JDK 21, project targets source 6
            ToolchainPluginStrategy strategy = new ToolchainPluginStrategy() {
                @Override
                int getRunningJdkMajor() {
                    return 21;
                }
            };

            String pomXml = """
                    <?xml version="1.0" encoding="UTF-8"?>
                    <project xmlns="http://maven.apache.org/POM/4.0.0">
                        <modelVersion>4.0.0</modelVersion>
                        <groupId>com.example</groupId>
                        <artifactId>test</artifactId>
                        <version>1.0</version>
                        <properties>
                            <maven.compiler.release>6</maven.compiler.release>
                        </properties>
                    </project>
                    """;
            Document doc = Document.of(pomXml);
            UpgradeContext context = TestUtils.createMockContext();

            UpgradeResult result = strategy.doApply(context, Map.of(POM_PATH, doc));

            assertEquals(1, result.modifiedPoms().size());
            assertTrue(strategy.hasToolchainsPluginWithSelectGoal(doc));
        }

        @Test
        @DisplayName("should not modify POM when source level is compatible")
        void noModificationWhenCompatible() {
            // Simulate running JDK 17, project targets source 11
            ToolchainPluginStrategy strategy = new ToolchainPluginStrategy() {
                @Override
                int getRunningJdkMajor() {
                    return 17;
                }
            };

            String pomXml = """
                    <?xml version="1.0" encoding="UTF-8"?>
                    <project xmlns="http://maven.apache.org/POM/4.0.0">
                        <modelVersion>4.0.0</modelVersion>
                        <groupId>com.example</groupId>
                        <artifactId>test</artifactId>
                        <version>1.0</version>
                        <properties>
                            <maven.compiler.release>11</maven.compiler.release>
                        </properties>
                    </project>
                    """;
            Document doc = Document.of(pomXml);
            UpgradeContext context = TestUtils.createMockContext();

            UpgradeResult result = strategy.doApply(context, Map.of(POM_PATH, doc));

            assertEquals(0, result.modifiedPoms().size());
        }

        @Test
        @DisplayName("should not modify POM when no source level configured")
        void noModificationWithoutSourceLevel() {
            ToolchainPluginStrategy strategy = new ToolchainPluginStrategy() {
                @Override
                int getRunningJdkMajor() {
                    return 21;
                }
            };

            String pomXml = """
                    <?xml version="1.0" encoding="UTF-8"?>
                    <project xmlns="http://maven.apache.org/POM/4.0.0">
                        <modelVersion>4.0.0</modelVersion>
                        <groupId>com.example</groupId>
                        <artifactId>test</artifactId>
                        <version>1.0</version>
                    </project>
                    """;
            Document doc = Document.of(pomXml);
            UpgradeContext context = TestUtils.createMockContext();

            UpgradeResult result = strategy.doApply(context, Map.of(POM_PATH, doc));

            assertEquals(0, result.modifiedPoms().size());
        }

        @Test
        @DisplayName("should not add duplicate plugin when already present")
        void noDuplicatePlugin() {
            ToolchainPluginStrategy strategy = new ToolchainPluginStrategy() {
                @Override
                int getRunningJdkMajor() {
                    return 21;
                }
            };

            String pomXml = """
                    <?xml version="1.0" encoding="UTF-8"?>
                    <project xmlns="http://maven.apache.org/POM/4.0.0">
                        <modelVersion>4.0.0</modelVersion>
                        <groupId>com.example</groupId>
                        <artifactId>test</artifactId>
                        <version>1.0</version>
                        <properties>
                            <maven.compiler.release>6</maven.compiler.release>
                        </properties>
                        <build>
                            <plugins>
                                <plugin>
                                    <groupId>org.apache.maven.plugins</groupId>
                                    <artifactId>maven-toolchains-plugin</artifactId>
                                    <executions>
                                        <execution>
                                            <goals>
                                                <goal>select-jdk-toolchain</goal>
                                            </goals>
                                        </execution>
                                    </executions>
                                </plugin>
                            </plugins>
                        </build>
                    </project>
                    """;
            Document doc = Document.of(pomXml);
            UpgradeContext context = TestUtils.createMockContext();

            UpgradeResult result = strategy.doApply(context, Map.of(POM_PATH, doc));

            assertEquals(0, result.modifiedPoms().size());
        }
    }
}