DeduplicateDependenciesStrategyTest.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 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.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;

/**
 * Tests for {@link DeduplicateDependenciesStrategy}.
 */
@DisplayName("DeduplicateDependenciesStrategy")
class DeduplicateDependenciesStrategyTest {

    private DeduplicateDependenciesStrategy strategy;

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

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

    @Nested
    @DisplayName("Duplicate Dependency Removal")
    class DuplicateDependencyTests {

        @Test
        @DisplayName("should remove duplicate dependencies keeping last occurrence")
        void shouldRemoveDuplicateDependenciesKeepingLast() 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>
                    <dependencies>
                        <dependency>
                            <groupId>commons-io</groupId>
                            <artifactId>commons-io</artifactId>
                            <version>2.11.0</version>
                        </dependency>
                        <dependency>
                            <groupId>commons-io</groupId>
                            <artifactId>commons-io</artifactId>
                            <version>2.15.0</version>
                        </dependency>
                    </dependencies>
                </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(), "Strategy should succeed");
            assertEquals(1, result.modifiedCount(), "Should have modified 1 POM");

            String xml = DomUtils.toXml(document);
            // Should keep last (2.15.0), remove first (2.11.0)
            assertTrue(xml.contains("<version>2.15.0</version>"), "Should keep last version");
            assertFalse(xml.contains("<version>2.11.0</version>"), "Should remove first version");

            Element deps = document.root().childElement("dependencies").orElse(null);
            long count = deps.childElements("dependency").count();
            assertEquals(1, count, "Should have only one dependency after dedup");
        }

        @Test
        @DisplayName("should remove duplicate in dependencyManagement keeping last")
        void shouldRemoveDuplicateInDependencyManagement() 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>
                    <dependencyManagement>
                        <dependencies>
                            <dependency>
                                <groupId>org.slf4j</groupId>
                                <artifactId>slf4j-api</artifactId>
                                <version>1.7.36</version>
                            </dependency>
                            <dependency>
                                <groupId>org.slf4j</groupId>
                                <artifactId>slf4j-api</artifactId>
                                <version>2.0.9</version>
                            </dependency>
                        </dependencies>
                    </dependencyManagement>
                </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(), "Strategy should succeed");
            assertEquals(1, result.modifiedCount());

            String xml = DomUtils.toXml(document);
            assertTrue(xml.contains("<version>2.0.9</version>"), "Should keep last version");
            assertFalse(xml.contains("<version>1.7.36</version>"), "Should remove first version");
        }

        @Test
        @DisplayName("should keep last when three duplicates exist")
        void shouldKeepLastOfThreeDuplicates() 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>
                    <dependencies>
                        <dependency>
                            <groupId>commons-io</groupId>
                            <artifactId>commons-io</artifactId>
                            <version>1.0</version>
                        </dependency>
                        <dependency>
                            <groupId>commons-io</groupId>
                            <artifactId>commons-io</artifactId>
                            <version>2.0</version>
                        </dependency>
                        <dependency>
                            <groupId>commons-io</groupId>
                            <artifactId>commons-io</artifactId>
                            <version>3.0</version>
                        </dependency>
                    </dependencies>
                </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());
            assertEquals(1, result.modifiedCount());

            String xml = DomUtils.toXml(document);
            assertTrue(xml.contains("<version>3.0</version>"), "Should keep version 3.0 (last)");
            assertFalse(xml.contains("<version>1.0</version>"), "Should remove version 1.0");
            assertFalse(xml.contains("<version>2.0</version>"), "Should remove version 2.0");

            Element deps = document.root().childElement("dependencies").orElse(null);
            assertEquals(1, deps.childElements("dependency").count());
        }

        @Test
        @DisplayName("should handle duplicates with same type and classifier")
        void shouldHandleDuplicatesWithTypeAndClassifier() 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>
                    <dependencies>
                        <dependency>
                            <groupId>io.netty</groupId>
                            <artifactId>netty-transport</artifactId>
                            <version>4.1.0</version>
                            <type>jar</type>
                            <classifier>linux-x86_64</classifier>
                        </dependency>
                        <dependency>
                            <groupId>io.netty</groupId>
                            <artifactId>netty-transport</artifactId>
                            <version>4.2.0</version>
                            <type>jar</type>
                            <classifier>linux-x86_64</classifier>
                        </dependency>
                    </dependencies>
                </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());
            assertEquals(1, result.modifiedCount());

            String xml = DomUtils.toXml(document);
            assertTrue(xml.contains("<version>4.2.0</version>"));
            assertFalse(xml.contains("<version>4.1.0</version>"));
        }

        @Test
        @DisplayName("should not treat different classifiers as duplicates")
        void shouldNotDedupDifferentClassifiers() 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>
                    <dependencies>
                        <dependency>
                            <groupId>io.netty</groupId>
                            <artifactId>netty-transport</artifactId>
                            <version>4.1.0</version>
                            <classifier>linux-x86_64</classifier>
                        </dependency>
                        <dependency>
                            <groupId>io.netty</groupId>
                            <artifactId>netty-transport</artifactId>
                            <version>4.1.0</version>
                            <classifier>linux-aarch_64</classifier>
                        </dependency>
                    </dependencies>
                </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());
            assertEquals(0, result.modifiedCount(), "Different classifiers are not duplicates");
        }

        @Test
        @DisplayName("should handle profile-scoped duplicate dependencies")
        void shouldHandleProfileDuplicateDependencies() 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>
                    <profiles>
                        <profile>
                            <id>test-profile</id>
                            <dependencies>
                                <dependency>
                                    <groupId>junit</groupId>
                                    <artifactId>junit</artifactId>
                                    <version>4.12</version>
                                </dependency>
                                <dependency>
                                    <groupId>junit</groupId>
                                    <artifactId>junit</artifactId>
                                    <version>4.13.2</version>
                                </dependency>
                            </dependencies>
                        </profile>
                    </profiles>
                </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());
            assertEquals(1, result.modifiedCount());

            String xml = DomUtils.toXml(document);
            assertTrue(xml.contains("<version>4.13.2</version>"), "Should keep last version");
            assertFalse(xml.contains("<version>4.12</version>"), "Should remove first version");
        }

        @Test
        @DisplayName("should keep property references in version")
        void shouldKeepPropertyVersion() 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>
                    <dependencies>
                        <dependency>
                            <groupId>commons-io</groupId>
                            <artifactId>commons-io</artifactId>
                            <version>2.11.0</version>
                        </dependency>
                        <dependency>
                            <groupId>commons-io</groupId>
                            <artifactId>commons-io</artifactId>
                            <version>${commons.io.version}</version>
                        </dependency>
                    </dependencies>
                </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());
            assertEquals(1, result.modifiedCount());

            String xml = DomUtils.toXml(document);
            assertTrue(
                    xml.contains("${commons.io.version}"),
                    "Should keep the last occurrence which has property version");
            assertFalse(xml.contains("<version>2.11.0</version>"));
        }

        @Test
        @DisplayName("should not modify when no duplicates exist")
        void shouldNotModifyWhenNoDuplicates() 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>
                    <dependencies>
                        <dependency>
                            <groupId>commons-io</groupId>
                            <artifactId>commons-io</artifactId>
                            <version>2.15.0</version>
                        </dependency>
                        <dependency>
                            <groupId>commons-lang</groupId>
                            <artifactId>commons-lang3</artifactId>
                            <version>3.14.0</version>
                        </dependency>
                    </dependencies>
                </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());
            assertEquals(0, result.modifiedCount(), "Should not modify when no duplicates");
        }
    }

    @Nested
    @DisplayName("Duplicate Plugin Removal")
    class DuplicatePluginTests {

        @Test
        @DisplayName("should remove duplicate plugins keeping last occurrence")
        void shouldRemoveDuplicatePlugins() 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.11.0</version>
                            </plugin>
                            <plugin>
                                <groupId>org.apache.maven.plugins</groupId>
                                <artifactId>maven-compiler-plugin</artifactId>
                                <version>3.13.0</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());
            assertEquals(1, result.modifiedCount());

            String xml = DomUtils.toXml(document);
            assertTrue(xml.contains("<version>3.13.0</version>"), "Should keep last version");
            assertFalse(xml.contains("<version>3.11.0</version>"), "Should remove first version");
        }

        @Test
        @DisplayName("should remove duplicate plugins in pluginManagement")
        void shouldRemoveDuplicatePluginsInPluginManagement() 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-surefire-plugin</artifactId>
                                    <version>3.1.0</version>
                                </plugin>
                                <plugin>
                                    <groupId>org.apache.maven.plugins</groupId>
                                    <artifactId>maven-surefire-plugin</artifactId>
                                    <version>3.2.5</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());
            assertEquals(1, result.modifiedCount());

            String xml = DomUtils.toXml(document);
            assertTrue(xml.contains("<version>3.2.5</version>"), "Should keep last version");
            assertFalse(xml.contains("<version>3.1.0</version>"), "Should remove first version");
        }

        @Test
        @DisplayName("should default groupId for maven plugins without explicit groupId")
        void shouldDefaultGroupIdForMavenPlugins() 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-jar-plugin</artifactId>
                                <version>3.3.0</version>
                            </plugin>
                            <plugin>
                                <groupId>org.apache.maven.plugins</groupId>
                                <artifactId>maven-jar-plugin</artifactId>
                                <version>3.4.0</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());
            assertEquals(1, result.modifiedCount(), "Should detect duplicate even without explicit groupId");

            String xml = DomUtils.toXml(document);
            assertTrue(xml.contains("<version>3.4.0</version>"), "Should keep last version");
        }
    }

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

        @Test
        @DisplayName("should be applicable with default options")
        void shouldBeApplicableWithDefaults() {
            UpgradeContext context = createMockContext();
            assertTrue(strategy.isApplicable(context));
        }

        @Test
        @DisplayName("should be applicable when --all is set")
        void shouldBeApplicableWithAll() {
            UpgradeContext context = TestUtils.createMockContext(TestUtils.createOptionsWithAll(true));
            assertTrue(strategy.isApplicable(context));
        }

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

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