PluginUpgradeStrategyTest.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.io.StringReader;
import java.io.StringWriter;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import org.apache.maven.api.cli.mvnup.UpgradeOptions;
import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.Namespace;
import org.jdom2.input.SAXBuilder;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;
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.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
 * Unit tests for the {@link PluginUpgradeStrategy} class.
 * Tests plugin version upgrades, plugin management additions, and Maven 4 compatibility.
 */
@DisplayName("PluginUpgradeStrategy")
class PluginUpgradeStrategyTest {

    private PluginUpgradeStrategy strategy;
    private SAXBuilder saxBuilder;

    @BeforeEach
    void setUp() {
        strategy = new PluginUpgradeStrategy();
        saxBuilder = new SAXBuilder();
    }

    private UpgradeContext createMockContext() {
        return TestUtils.createMockContext();
    }

    private UpgradeContext createMockContext(UpgradeOptions options) {
        return TestUtils.createMockContext(options);
    }

    private UpgradeOptions createDefaultOptions() {
        return TestUtils.createDefaultOptions();
    }

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

        @Test
        @DisplayName("should be applicable when --plugins option is true")
        void shouldBeApplicableWhenPluginsOptionTrue() {
            UpgradeOptions options = mock(UpgradeOptions.class);
            when(options.plugins()).thenReturn(Optional.of(true));
            when(options.all()).thenReturn(Optional.empty());

            UpgradeContext context = createMockContext(options);

            assertTrue(strategy.isApplicable(context), "Strategy should be applicable when --plugins is true");
        }

        @Test
        @DisplayName("should be applicable when --all option is specified")
        void shouldBeApplicableWhenAllOptionSpecified() {
            UpgradeOptions options = mock(UpgradeOptions.class);
            when(options.all()).thenReturn(Optional.of(true));
            when(options.plugins()).thenReturn(Optional.empty());

            UpgradeContext context = createMockContext(options);

            assertTrue(strategy.isApplicable(context), "Strategy should be applicable when --all is specified");
        }

        @Test
        @DisplayName("should be applicable by default when no specific options provided")
        void shouldBeApplicableByDefaultWhenNoSpecificOptions() {
            UpgradeOptions options = createDefaultOptions();

            UpgradeContext context = createMockContext(options);

            assertTrue(strategy.isApplicable(context), "Strategy should be applicable by default");
        }

        @Test
        @DisplayName("should not be applicable when --plugins option is false")
        void shouldNotBeApplicableWhenPluginsOptionFalse() {
            UpgradeOptions options = mock(UpgradeOptions.class);
            when(options.plugins()).thenReturn(Optional.of(false));
            when(options.all()).thenReturn(Optional.empty());

            UpgradeContext context = createMockContext(options);

            assertFalse(strategy.isApplicable(context), "Strategy should not be applicable when --plugins is false");
        }
    }

    @Nested
    @DisplayName("Plugin Upgrades")
    class PluginUpgradeTests {

        @Test
        @DisplayName("should upgrade plugin version when below minimum")
        void shouldUpgradePluginVersionWhenBelowMinimum() throws Exception {
            String pomXml = PomBuilder.create()
                    .groupId("test")
                    .artifactId("test")
                    .version("1.0.0")
                    .plugin("org.apache.maven.plugins", "maven-compiler-plugin", "3.8.1")
                    .build();

            Document document = saxBuilder.build(new StringReader(pomXml));
            Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);

            UpgradeContext context = createMockContext();
            UpgradeResult result = strategy.apply(context, pomMap);

            assertTrue(result.success(), "Plugin upgrade should succeed");
            // Note: POM may or may not be modified depending on whether upgrades are needed

            // Verify the plugin version was upgraded
            Element root = document.getRootElement();
            Namespace namespace = root.getNamespace();
            Element build = root.getChild("build", namespace);
            Element plugins = build.getChild("plugins", namespace);
            Element plugin = plugins.getChild("plugin", namespace);
            String version = plugin.getChildText("version", namespace);

            // The exact version depends on the plugin upgrades configuration
            assertNotNull(version, "Plugin should have a version");
        }

        @Test
        @DisplayName("should not modify plugin when version is already sufficient")
        void shouldNotModifyPluginWhenVersionAlreadySufficient() throws Exception {
            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>test</groupId>
                    <artifactId>test</artifactId>
                    <version>1.0.0</version>
                    <build>
                        <plugins>
                            <plugin>
                                <groupId>org.apache.maven.plugins</groupId>
                                <artifactId>maven-compiler-plugin</artifactId>
                                <version>3.13.0</version>
                            </plugin>
                        </plugins>
                    </build>
                </project>
                """;

            Document document = saxBuilder.build(new StringReader(pomXml));
            Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);

            UpgradeContext context = createMockContext();
            UpgradeResult result = strategy.apply(context, pomMap);

            assertTrue(result.success(), "Plugin upgrade should succeed");
            // POM might still be marked as modified due to other plugin management additions
        }

        @Test
        @DisplayName("should upgrade plugin in pluginManagement")
        void shouldUpgradePluginInPluginManagement() throws Exception {
            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>test</groupId>
                    <artifactId>test</artifactId>
                    <version>1.0.0</version>
                    <build>
                        <pluginManagement>
                            <plugins>
                                <plugin>
                                    <groupId>org.apache.maven.plugins</groupId>
                                    <artifactId>maven-enforcer-plugin</artifactId>
                                    <version>2.0.0</version>
                                </plugin>
                            </plugins>
                        </pluginManagement>
                    </build>
                </project>
                """;

            Document document = saxBuilder.build(new StringReader(pomXml));
            Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);

            UpgradeContext context = createMockContext();
            UpgradeResult result = strategy.apply(context, pomMap);

            assertTrue(result.success(), "Plugin upgrade should succeed");
            assertTrue(result.modifiedCount() > 0, "Should have upgraded maven-enforcer-plugin");

            // Verify the version was upgraded
            Element root = document.getRootElement();
            Namespace namespace = root.getNamespace();
            Element pluginElement = root.getChild("build", namespace)
                    .getChild("pluginManagement", namespace)
                    .getChild("plugins", namespace)
                    .getChild("plugin", namespace);
            Element versionElement = pluginElement.getChild("version", namespace);
            assertEquals("3.0.0", versionElement.getTextTrim());
        }

        @Test
        @DisplayName("should upgrade plugin with property version")
        void shouldUpgradePluginWithPropertyVersion() throws Exception {
            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>test</groupId>
                    <artifactId>test</artifactId>
                    <version>1.0.0</version>
                    <properties>
                        <shade.plugin.version>3.0.0</shade.plugin.version>
                    </properties>
                    <build>
                        <plugins>
                            <plugin>
                                <groupId>org.apache.maven.plugins</groupId>
                                <artifactId>maven-shade-plugin</artifactId>
                                <version>${shade.plugin.version}</version>
                            </plugin>
                        </plugins>
                    </build>
                </project>
                """;

            Document document = saxBuilder.build(new StringReader(pomXml));
            Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);

            UpgradeContext context = createMockContext();
            UpgradeResult result = strategy.apply(context, pomMap);

            assertTrue(result.success(), "Plugin upgrade should succeed");
            assertTrue(result.modifiedCount() > 0, "Should have upgraded shade plugin property");

            // Verify the property was upgraded
            Element root = document.getRootElement();
            Namespace namespace = root.getNamespace();
            Element propertyElement =
                    root.getChild("properties", namespace).getChild("shade.plugin.version", namespace);
            assertEquals("3.5.0", propertyElement.getTextTrim());
        }

        @Test
        @DisplayName("should not upgrade when version is already higher")
        void shouldNotUpgradeWhenVersionAlreadyHigher() throws Exception {
            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>test</groupId>
                    <artifactId>test</artifactId>
                    <version>1.0.0</version>
                    <build>
                        <plugins>
                            <plugin>
                                <groupId>org.codehaus.mojo</groupId>
                                <artifactId>flatten-maven-plugin</artifactId>
                                <version>1.3.0</version>
                            </plugin>
                        </plugins>
                    </build>
                </project>
                """;

            Document document = saxBuilder.build(new StringReader(pomXml));
            Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);

            UpgradeContext context = createMockContext();
            UpgradeResult result = strategy.apply(context, pomMap);

            assertTrue(result.success(), "Plugin upgrade should succeed");

            // Verify the version was not changed
            Element root = document.getRootElement();
            Namespace namespace = root.getNamespace();
            Element pluginElement = root.getChild("build", namespace)
                    .getChild("plugins", namespace)
                    .getChild("plugin", namespace);
            Element versionElement = pluginElement.getChild("version", namespace);
            assertEquals("1.3.0", versionElement.getTextTrim());
        }

        @Test
        @DisplayName("should upgrade plugin without explicit groupId")
        void shouldUpgradePluginWithoutExplicitGroupId() throws Exception {
            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>test</groupId>
                    <artifactId>test</artifactId>
                    <version>1.0.0</version>
                    <build>
                        <plugins>
                            <plugin>
                                <artifactId>maven-shade-plugin</artifactId>
                                <version>3.1.0</version>
                            </plugin>
                        </plugins>
                    </build>
                </project>
                """;

            Document document = saxBuilder.build(new StringReader(pomXml));
            Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);

            UpgradeContext context = createMockContext();
            UpgradeResult result = strategy.apply(context, pomMap);

            assertTrue(result.success(), "Plugin upgrade should succeed");
            assertTrue(
                    result.modifiedCount() > 0,
                    "Should have upgraded maven-shade-plugin even without explicit groupId");

            // Verify the version was upgraded
            Element root = document.getRootElement();
            Namespace namespace = root.getNamespace();
            Element pluginElement = root.getChild("build", namespace)
                    .getChild("plugins", namespace)
                    .getChild("plugin", namespace);
            Element versionElement = pluginElement.getChild("version", namespace);
            assertEquals("3.5.0", versionElement.getTextTrim());
        }

        @Test
        @DisplayName("should not upgrade plugin without version")
        void shouldNotUpgradePluginWithoutVersion() throws Exception {
            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>test</groupId>
                    <artifactId>test</artifactId>
                    <version>1.0.0</version>
                    <build>
                        <plugins>
                            <plugin>
                                <groupId>org.apache.maven.plugins</groupId>
                                <artifactId>maven-exec-plugin</artifactId>
                                <!-- No version - inherited from parent or pluginManagement -->
                            </plugin>
                        </plugins>
                    </build>
                </project>
                """;

            Document document = saxBuilder.build(new StringReader(pomXml));
            Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);

            UpgradeContext context = createMockContext();
            UpgradeResult result = strategy.apply(context, pomMap);

            assertTrue(result.success(), "Plugin upgrade should succeed");
            // Note: POM might still be modified due to plugin management additions
        }

        @Test
        @DisplayName("should not upgrade when property is not found")
        void shouldNotUpgradeWhenPropertyNotFound() throws Exception {
            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>test</groupId>
                    <artifactId>test</artifactId>
                    <version>1.0.0</version>
                    <build>
                        <plugins>
                            <plugin>
                                <groupId>org.apache.maven.plugins</groupId>
                                <artifactId>maven-exec-plugin</artifactId>
                                <version>${exec.plugin.version}</version>
                            </plugin>
                        </plugins>
                    </build>
                </project>
                """;

            Document document = saxBuilder.build(new StringReader(pomXml));
            Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);

            UpgradeContext context = createMockContext();
            UpgradeResult result = strategy.apply(context, pomMap);

            assertTrue(result.success(), "Plugin upgrade should succeed");
            // Note: POM might still be modified due to plugin management additions
        }
    }

    @Nested
    @DisplayName("Plugin Management")
    class PluginManagementTests {

        @Test
        @DisplayName("should add pluginManagement before existing plugins section")
        void shouldAddPluginManagementBeforeExistingPluginsSection() throws Exception {
            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>test</groupId>
                    <artifactId>test</artifactId>
                    <version>1.0.0</version>
                    <build>
                        <plugins>
                            <plugin>
                                <groupId>org.apache.maven.plugins</groupId>
                                <artifactId>maven-compiler-plugin</artifactId>
                                <version>3.8.1</version>
                            </plugin>
                        </plugins>
                    </build>
                </project>
                """;

            Document document = saxBuilder.build(new StringReader(pomXml));
            Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);

            UpgradeContext context = createMockContext();
            strategy.apply(context, pomMap);

            // Verify the structure
            Element root = document.getRootElement();
            Namespace namespace = root.getNamespace();
            Element buildElement = root.getChild("build", namespace);
            assertNotNull(buildElement, "Build element should exist");

            List<Element> buildChildren = buildElement.getChildren();

            // Find the indices of pluginManagement and plugins
            int pluginManagementIndex = -1;
            int pluginsIndex = -1;

            for (int i = 0; i < buildChildren.size(); i++) {
                Element child = buildChildren.get(i);
                if ("pluginManagement".equals(child.getName())) {
                    pluginManagementIndex = i;
                } else if ("plugins".equals(child.getName())) {
                    pluginsIndex = i;
                }
            }

            assertTrue(pluginsIndex >= 0, "plugins should be present");
            if (pluginManagementIndex >= 0) {
                assertTrue(
                        pluginManagementIndex < pluginsIndex,
                        "pluginManagement should come before plugins when both are present");
            }
        }
    }

    @Nested
    @DisplayName("Plugin Upgrade Configuration")
    class PluginUpgradeConfigurationTests {

        @Test
        @DisplayName("should have predefined plugin upgrades")
        void shouldHavePredefinedPluginUpgrades() throws Exception {
            List<PluginUpgrade> upgrades = PluginUpgradeStrategy.getPluginUpgrades();

            assertFalse(upgrades.isEmpty(), "Should have predefined plugin upgrades");

            // Verify some expected plugins are included
            boolean hasCompilerPlugin =
                    upgrades.stream().anyMatch(upgrade -> "maven-compiler-plugin".equals(upgrade.artifactId()));
            boolean hasExecPlugin =
                    upgrades.stream().anyMatch(upgrade -> "maven-exec-plugin".equals(upgrade.artifactId()));

            assertTrue(hasCompilerPlugin, "Should include maven-compiler-plugin upgrade");
            assertTrue(hasExecPlugin, "Should include maven-exec-plugin upgrade");
        }

        @Test
        @DisplayName("should have valid plugin upgrade definitions")
        void shouldHaveValidPluginUpgradeDefinitions() throws Exception {
            List<PluginUpgrade> upgrades = PluginUpgradeStrategy.getPluginUpgrades();

            for (PluginUpgrade upgrade : upgrades) {
                assertNotNull(upgrade.groupId(), "Plugin upgrade should have groupId");
                assertNotNull(upgrade.artifactId(), "Plugin upgrade should have artifactId");
                assertNotNull(upgrade.minVersion(), "Plugin upgrade should have minVersion");
                // configuration can be null for some plugins
            }
        }
    }

    @Nested
    @DisplayName("Error Handling")
    class ErrorHandlingTests {

        @Test
        @DisplayName("should handle malformed POM gracefully")
        void shouldHandleMalformedPOMGracefully() throws Exception {
            String malformedPomXml =
                    """
                <?xml version="1.0" encoding="UTF-8"?>
                <project xmlns="http://maven.apache.org/POM/4.0.0">
                    <modelVersion>4.0.0</modelVersion>
                    <groupId>test</groupId>
                    <artifactId>test</artifactId>
                    <version>1.0.0</version>
                    <build>
                        <plugins>
                            <plugin>
                                <!-- Missing required elements -->
                            </plugin>
                        </plugins>
                    </build>
                </project>
                """;

            Document document = saxBuilder.build(new StringReader(malformedPomXml));
            Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);

            UpgradeContext context = createMockContext();
            UpgradeResult result = strategy.apply(context, pomMap);

            // Strategy should handle malformed POMs gracefully
            assertNotNull(result, "Result should not be null");
            assertTrue(result.processedPoms().contains(Paths.get("pom.xml")), "POM should be marked as processed");
        }
    }

    @Nested
    @DisplayName("Strategy Description")
    class StrategyDescriptionTests {

        @Test
        @DisplayName("should provide meaningful description")
        void shouldProvideMeaningfulDescription() {
            String description = strategy.getDescription();

            assertNotNull(description, "Description should not be null");
            assertFalse(description.trim().isEmpty(), "Description should not be empty");
            assertTrue(description.toLowerCase().contains("plugin"), "Description should mention plugins");
        }
    }

    @Nested
    @DisplayName("XML Formatting")
    class XmlFormattingTests {

        @Test
        @DisplayName("should format pluginManagement with proper indentation")
        void shouldFormatPluginManagementWithProperIndentation() throws Exception {
            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>test</groupId>
                    <artifactId>test</artifactId>
                    <version>1.0.0</version>
                    <build>
                        <plugins>
                            <plugin>
                                <groupId>org.apache.maven.plugins</groupId>
                                <artifactId>maven-compiler-plugin</artifactId>
                                <version>3.1</version>
                            </plugin>
                        </plugins>
                    </build>
                </project>
                """;

            Document document = saxBuilder.build(new StringReader(pomXml));
            Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);

            UpgradeContext context = createMockContext();
            strategy.apply(context, pomMap);

            // Convert to string to check formatting
            Format format = Format.getRawFormat();
            format.setLineSeparator(System.lineSeparator());
            XMLOutputter out = new XMLOutputter(format);
            StringWriter writer = new StringWriter();
            out.output(document.getRootElement(), writer);
            String result = writer.toString();

            // Check that the plugin version was upgraded
            assertTrue(result.contains("<version>3.2</version>"), "Plugin version should be upgraded to 3.2");

            // Verify that the XML formatting is correct - no malformed closing tags
            assertFalse(result.contains("</plugin></plugins>"), "Should not have malformed closing tags");
            assertFalse(result.contains("</plugins></pluginManagement>"), "Should not have malformed closing tags");

            // Check that proper indentation is maintained
            assertTrue(result.contains("    <build>"), "Build element should be properly indented");
            assertTrue(result.contains("        <plugins>"), "Plugins element should be properly indented");
            assertTrue(result.contains("            <plugin>"), "Plugin element should be properly indented");
        }

        @Test
        @DisplayName("should format pluginManagement with proper indentation when added")
        void shouldFormatPluginManagementWithProperIndentationWhenAdded() throws Exception {
            // Use a POM that will trigger pluginManagement addition by having a plugin without version
            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>test</groupId>
                    <artifactId>test</artifactId>
                    <version>1.0.0</version>
                    <build>
                        <plugins>
                            <plugin>
                                <groupId>org.apache.maven.plugins</groupId>
                                <artifactId>maven-enforcer-plugin</artifactId>
                                <!-- No version - should trigger pluginManagement addition -->
                            </plugin>
                        </plugins>
                    </build>
                </project>
                """;

            Document document = saxBuilder.build(new StringReader(pomXml));
            Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);

            UpgradeContext context = createMockContext();
            strategy.apply(context, pomMap);

            // Convert to string to check formatting
            Format format = Format.getRawFormat();
            format.setLineSeparator(System.lineSeparator());
            XMLOutputter out = new XMLOutputter(format);
            StringWriter writer = new StringWriter();
            out.output(document.getRootElement(), writer);
            String result = writer.toString();

            // If pluginManagement was added, verify proper formatting
            if (result.contains("<pluginManagement>")) {
                // Verify that the XML formatting is correct - no malformed closing tags
                assertFalse(result.contains("</plugin></plugins>"), "Should not have malformed closing tags");
                assertFalse(result.contains("</plugins></pluginManagement>"), "Should not have malformed closing tags");

                // Check that proper indentation is maintained for pluginManagement
                assertTrue(
                        result.contains("        <pluginManagement>"), "PluginManagement should be properly indented");
                assertTrue(
                        result.contains("            <plugins>"),
                        "Plugins in pluginManagement should be properly indented");
                assertTrue(
                        result.contains("        </pluginManagement>"),
                        "PluginManagement closing tag should be properly indented");
            }
        }
    }
}