PluginUpgradeQuarkusTest.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.List;
import java.util.Map;

import eu.maveniverse.domtrip.Document;
import eu.maveniverse.domtrip.Editor;
import eu.maveniverse.domtrip.Element;
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.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.verify;

/**
 * Tests for Quarkus plugin upgrade behavior in {@link PluginUpgradeStrategy}.
 * Extracted from PluginUpgradeStrategyTest to keep file lengths within the Checkstyle limit.
 */
@DisplayName("PluginUpgradeStrategy ��� Quarkus Plugin Upgrades")
class PluginUpgradeQuarkusTest {

    private PluginUpgradeStrategy strategy;

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

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

    @Test
    @DisplayName("should upgrade quarkus-maven-plugin with io.quarkus groupId when below minimum")
    void shouldUpgradeQuarkusPluginWithIoQuarkusGroupId() 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>io.quarkus</groupId>
                                <artifactId>quarkus-maven-plugin</artifactId>
                                <version>3.16.3</version>
                            </plugin>
                        </plugins>
                    </build>
                </project>
                """;

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

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

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

        Editor editor = new Editor(document);
        String version = editor.root()
                .path("build", "plugins", "plugin", "version")
                .map(Element::textContentTrimmed)
                .orElse(null);
        assertEquals("3.26.0", version, "quarkus-maven-plugin should be upgraded to 3.26.0");
    }

    @Test
    @DisplayName("should upgrade quarkus-maven-plugin with io.quarkus.platform groupId when below minimum")
    void shouldUpgradeQuarkusPluginWithPlatformGroupId() 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>io.quarkus.platform</groupId>
                                <artifactId>quarkus-maven-plugin</artifactId>
                                <version>3.16.3</version>
                            </plugin>
                        </plugins>
                    </build>
                </project>
                """;

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

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

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

        Editor editor = new Editor(document);
        String version = editor.root()
                .path("build", "plugins", "plugin", "version")
                .map(Element::textContentTrimmed)
                .orElse(null);
        assertEquals("3.26.0", version, "quarkus-maven-plugin should be upgraded to 3.26.0");
    }

    @Test
    @DisplayName("should not upgrade quarkus-maven-plugin when version is already sufficient")
    void shouldNotUpgradeQuarkusPluginWhenVersionSufficient() 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>io.quarkus</groupId>
                                <artifactId>quarkus-maven-plugin</artifactId>
                                <version>3.31.4</version>
                            </plugin>
                        </plugins>
                    </build>
                </project>
                """;

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

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

        Editor editor = new Editor(document);
        String version = editor.root()
                .path("build", "plugins", "plugin", "version")
                .map(Element::textContentTrimmed)
                .orElse(null);
        assertEquals("3.31.4", version, "Version 3.31.4 should be preserved");
    }

    @Test
    @DisplayName("should decouple plugin version from shared BOM property")
    void shouldDecouplePluginVersionFromSharedBomProperty() 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>
                        <quarkus.platform.version>3.16.3</quarkus.platform.version>
                    </properties>
                    <dependencyManagement>
                        <dependencies>
                            <dependency>
                                <groupId>io.quarkus.platform</groupId>
                                <artifactId>quarkus-bom</artifactId>
                                <version>${quarkus.platform.version}</version>
                                <type>pom</type>
                                <scope>import</scope>
                            </dependency>
                        </dependencies>
                    </dependencyManagement>
                    <build>
                        <plugins>
                            <plugin>
                                <groupId>io.quarkus</groupId>
                                <artifactId>quarkus-maven-plugin</artifactId>
                                <version>${quarkus.platform.version}</version>
                            </plugin>
                        </plugins>
                    </build>
                </project>
                """;

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

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

        assertTrue(result.success(), "Plugin upgrade should succeed");
        assertTrue(result.modifiedCount() > 0, "Should have modified POM");

        // BOM property should be unchanged
        Editor editor = new Editor(document);
        String bomVersion = editor.root()
                .path("properties", "quarkus.platform.version")
                .map(Element::textContentTrimmed)
                .orElse(null);
        assertEquals("3.16.3", bomVersion, "BOM property should remain unchanged at 3.16.3");

        // New property should be introduced
        String pluginVersion = editor.root()
                .path("properties", "quarkus-plugin.version")
                .map(Element::textContentTrimmed)
                .orElse(null);
        assertEquals("3.26.0", pluginVersion, "New quarkus-plugin.version property should be 3.26.0");

        // Plugin version should reference the new property
        String pluginVersionRef = editor.root()
                .path("build", "plugins", "plugin", "version")
                .map(Element::textContentTrimmed)
                .orElse(null);
        assertEquals(
                "${quarkus-plugin.version}",
                pluginVersionRef,
                "Plugin should reference the new quarkus-plugin.version property");
    }

    @Test
    @DisplayName("should not decouple when plugin has its own property not shared with BOM")
    void shouldNotDecoupleWhenPluginHasOwnProperty() 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>
                        <quarkus-plugin.version>3.16.3</quarkus-plugin.version>
                    </properties>
                    <build>
                        <plugins>
                            <plugin>
                                <groupId>io.quarkus</groupId>
                                <artifactId>quarkus-maven-plugin</artifactId>
                                <version>${quarkus-plugin.version}</version>
                            </plugin>
                        </plugins>
                    </build>
                </project>
                """;

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

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

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

        // The property should be upgraded directly (no decoupling needed)
        Editor editor = new Editor(document);
        String version = editor.root()
                .path("properties", "quarkus-plugin.version")
                .map(Element::textContentTrimmed)
                .orElse(null);
        assertEquals("3.26.0", version, "Property should be upgraded directly to 3.26.0");

        // Plugin should still reference the same property
        String pluginVersionRef = editor.root()
                .path("build", "plugins", "plugin", "version")
                .map(Element::textContentTrimmed)
                .orElse(null);
        assertEquals("${quarkus-plugin.version}", pluginVersionRef, "Plugin should still reference the same property");
    }

    @Test
    @DisplayName("should not decouple when BOM version is already sufficient")
    void shouldNotDecoupleWhenBomVersionSufficient() 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>
                        <quarkus.platform.version>3.31.4</quarkus.platform.version>
                    </properties>
                    <dependencyManagement>
                        <dependencies>
                            <dependency>
                                <groupId>io.quarkus.platform</groupId>
                                <artifactId>quarkus-bom</artifactId>
                                <version>${quarkus.platform.version}</version>
                                <type>pom</type>
                                <scope>import</scope>
                            </dependency>
                        </dependencies>
                    </dependencyManagement>
                    <build>
                        <plugins>
                            <plugin>
                                <groupId>io.quarkus</groupId>
                                <artifactId>quarkus-maven-plugin</artifactId>
                                <version>${quarkus.platform.version}</version>
                            </plugin>
                        </plugins>
                    </build>
                </project>
                """;

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

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

        // No new property should be introduced ��� version is already sufficient
        Editor editor = new Editor(document);
        String bomVersion = editor.root()
                .path("properties", "quarkus.platform.version")
                .map(Element::textContentTrimmed)
                .orElse(null);
        assertEquals("3.31.4", bomVersion, "BOM property should remain unchanged");

        // No quarkus-plugin.version should exist
        Element newProp =
                editor.root().path("properties", "quarkus-plugin.version").orElse(null);
        assertTrue(newProp == null, "Should not introduce new property when version is already sufficient");
    }

    @Test
    @DisplayName("should emit version gap warning when decoupling")
    void shouldEmitVersionGapWarningWhenDecoupling() 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>
                        <quarkus.platform.version>3.16.3</quarkus.platform.version>
                    </properties>
                    <dependencyManagement>
                        <dependencies>
                            <dependency>
                                <groupId>io.quarkus.platform</groupId>
                                <artifactId>quarkus-bom</artifactId>
                                <version>${quarkus.platform.version}</version>
                                <type>pom</type>
                                <scope>import</scope>
                            </dependency>
                        </dependencies>
                    </dependencyManagement>
                    <build>
                        <plugins>
                            <plugin>
                                <groupId>io.quarkus</groupId>
                                <artifactId>quarkus-maven-plugin</artifactId>
                                <version>${quarkus.platform.version}</version>
                            </plugin>
                        </plugins>
                    </build>
                </project>
                """;

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

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

        // Verify warning was emitted about the version gap
        verify(context.logger, atLeastOnce())
                .warn(argThat(msg -> msg.contains("quarkus-maven-plugin upgraded to 3.26.0")
                        && msg.contains("3.16.3")
                        && msg.contains("mismatched plugin and platform")));
    }

    @Test
    @DisplayName("should include quarkus-maven-plugin in predefined plugin upgrades")
    void shouldIncludeQuarkusPluginInPredefinedUpgrades() {
        List<PluginUpgrade> upgrades = PluginUpgradeStrategy.getPluginUpgrades();

        boolean hasIoQuarkus = upgrades.stream()
                .anyMatch(u -> "io.quarkus".equals(u.groupId()) && "quarkus-maven-plugin".equals(u.artifactId()));
        boolean hasIoQuarkusPlatform = upgrades.stream()
                .anyMatch(u ->
                        "io.quarkus.platform".equals(u.groupId()) && "quarkus-maven-plugin".equals(u.artifactId()));

        assertTrue(hasIoQuarkus, "Should include io.quarkus:quarkus-maven-plugin upgrade");
        assertTrue(hasIoQuarkusPlatform, "Should include io.quarkus.platform:quarkus-maven-plugin upgrade");

        // Verify the reason text
        upgrades.stream()
                .filter(u -> "quarkus-maven-plugin".equals(u.artifactId()))
                .forEach(u -> assertEquals(
                        "Maven 4 compatibility (Aether API changes)",
                        u.reason(),
                        "Quarkus plugin upgrade should have the correct reason"));
    }

    @Test
    @DisplayName("should upgrade quarkus-maven-plugin in pluginManagement")
    void shouldUpgradeQuarkusPluginInPluginManagement() 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>io.quarkus</groupId>
                                    <artifactId>quarkus-maven-plugin</artifactId>
                                    <version>3.16.3</version>
                                </plugin>
                            </plugins>
                        </pluginManagement>
                    </build>
                </project>
                """;

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

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

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

        Editor editor = new Editor(document);
        String version = editor.root()
                .path("build", "pluginManagement", "plugins", "plugin", "version")
                .map(Element::textContentTrimmed)
                .orElse(null);
        assertEquals("3.26.0", version, "quarkus-maven-plugin in pluginManagement should be upgraded to 3.26.0");
    }

    @Test
    @DisplayName("should not decouple when shared property is inherited from parent POM")
    void shouldNotDecoupleWhenSharedPropertyIsInherited() throws Exception {
        // The shared property is NOT declared in this POM ��� it's inherited from a parent.
        // We cannot resolve its value, so decoupling should be skipped to avoid
        // introducing a quarkus-plugin.version=3.26.0 that might downgrade an already-sufficient 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>
                    <parent>
                        <groupId>org.example</groupId>
                        <artifactId>parent</artifactId>
                        <version>1.0.0</version>
                    </parent>
                    <artifactId>child</artifactId>
                    <dependencyManagement>
                        <dependencies>
                            <dependency>
                                <groupId>io.quarkus.platform</groupId>
                                <artifactId>quarkus-bom</artifactId>
                                <version>${quarkus.platform.version}</version>
                                <type>pom</type>
                                <scope>import</scope>
                            </dependency>
                        </dependencies>
                    </dependencyManagement>
                    <build>
                        <plugins>
                            <plugin>
                                <groupId>io.quarkus</groupId>
                                <artifactId>quarkus-maven-plugin</artifactId>
                                <version>${quarkus.platform.version}</version>
                            </plugin>
                        </plugins>
                    </build>
                </project>
                """;

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

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

        Editor editor = new Editor(document);
        Element newProp =
                editor.root().path("properties", "quarkus-plugin.version").orElse(null);
        assertTrue(newProp == null, "Should not introduce quarkus-plugin.version when shared property is inherited");

        // The plugin version reference should remain unchanged
        String pluginVersion = editor.root()
                .path("build", "plugins", "plugin", "version")
                .map(Element::textContentTrimmed)
                .orElse(null);
        assertEquals(
                "${quarkus.platform.version}",
                pluginVersion,
                "Plugin version should remain as the inherited property reference");
    }
}