CompatibilityFixStrategyTest.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.nio.file.Path;
import java.nio.file.Paths;
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.input.SAXBuilder;
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 CompatibilityFixStrategy} class.
 * Tests Maven 4 compatibility fixes including duplicate dependency and plugin handling.
 */
@DisplayName("CompatibilityFixStrategy")
class CompatibilityFixStrategyTest {

    private CompatibilityFixStrategy strategy;
    private SAXBuilder saxBuilder;

    @BeforeEach
    void setUp() {
        strategy = new CompatibilityFixStrategy();
        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 --model option is true")
        void shouldBeApplicableWhenModelOptionTrue() {
            UpgradeOptions options = mock(UpgradeOptions.class);
            when(options.model()).thenReturn(Optional.of(true));
            when(options.all()).thenReturn(Optional.empty());

            UpgradeContext context = createMockContext(options);

            assertTrue(strategy.isApplicable(context), "Strategy should be applicable when --model 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.model()).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 --model option is false")
        void shouldNotBeApplicableWhenModelOptionFalse() {
            UpgradeOptions options = mock(UpgradeOptions.class);
            when(options.model()).thenReturn(Optional.of(false));
            when(options.all()).thenReturn(Optional.empty());

            UpgradeContext context = createMockContext(options);

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

        @Test
        @DisplayName("should handle all options disabled")
        void shouldHandleAllOptionsDisabled() {
            UpgradeContext context = TestUtils.createMockContext(TestUtils.createOptions(
                    false, // --all
                    false, // --infer
                    false, // --fix-model
                    false, // --plugins
                    null // --model
                    ));

            // Should apply default behavior when all options are explicitly disabled
            assertTrue(
                    strategy.isApplicable(context),
                    "Strategy should apply default behavior when all options are disabled");
        }
    }

    @Nested
    @DisplayName("Duplicate Dependency Fixes")
    class DuplicateDependencyFixesTests {

        @Test
        @DisplayName("should remove duplicate dependencies in dependencyManagement")
        void shouldRemoveDuplicateDependenciesInDependencyManagement() 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.apache.commons</groupId>
                                <artifactId>commons-lang3</artifactId>
                                <version>3.12.0</version>
                            </dependency>
                            <dependency>
                                <groupId>org.apache.commons</groupId>
                                <artifactId>commons-lang3</artifactId>
                                <version>3.13.0</version>
                            </dependency>
                        </dependencies>
                    </dependencyManagement>
                </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(), "Compatibility fix should succeed");
            assertTrue(result.modifiedCount() > 0, "Should have removed duplicate dependency");

            // Verify only one dependency remains
            Element root = document.getRootElement();
            Element dependencyManagement = root.getChild("dependencyManagement", root.getNamespace());
            Element dependencies = dependencyManagement.getChild("dependencies", root.getNamespace());
            assertEquals(
                    1,
                    dependencies.getChildren("dependency", root.getNamespace()).size(),
                    "Should have only one dependency after duplicate removal");
        }

        @Test
        @DisplayName("should remove duplicate dependencies in regular dependencies")
        void shouldRemoveDuplicateDependenciesInRegularDependencies() 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>junit</groupId>
                            <artifactId>junit</artifactId>
                            <version>4.13.2</version>
                            <scope>test</scope>
                        </dependency>
                        <dependency>
                            <groupId>junit</groupId>
                            <artifactId>junit</artifactId>
                            <version>4.13.2</version>
                            <scope>test</scope>
                        </dependency>
                    </dependencies>
                </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(), "Compatibility fix should succeed");
            assertTrue(result.modifiedCount() > 0, "Should have removed duplicate dependency");

            // Verify only one dependency remains
            Element root = document.getRootElement();
            Element dependencies = root.getChild("dependencies", root.getNamespace());
            assertEquals(
                    1,
                    dependencies.getChildren("dependency", root.getNamespace()).size(),
                    "Should have only one dependency after duplicate removal");
        }
    }

    @Nested
    @DisplayName("Duplicate Plugin Fixes")
    class DuplicatePluginFixesTests {

        @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-compiler-plugin</artifactId>
                                    <version>3.11.0</version>
                                </plugin>
                                <plugin>
                                    <groupId>org.apache.maven.plugins</groupId>
                                    <artifactId>maven-compiler-plugin</artifactId>
                                    <version>3.12.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(), "Compatibility fix should succeed");
            assertTrue(result.modifiedCount() > 0, "Should have removed duplicate plugin");

            // Verify only one plugin remains
            Element root = document.getRootElement();
            Element build = root.getChild("build", root.getNamespace());
            Element pluginManagement = build.getChild("pluginManagement", root.getNamespace());
            Element plugins = pluginManagement.getChild("plugins", root.getNamespace());
            assertEquals(
                    1,
                    plugins.getChildren("plugin", root.getNamespace()).size(),
                    "Should have only one plugin after duplicate removal");
        }
    }

    @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("compatibility")
                            || description.toLowerCase().contains("fix"),
                    "Description should mention compatibility or fix");
        }
    }
}