InferenceStrategyTest.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.HashMap;
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.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
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 InferenceStrategy} class.
 * Tests Maven 4.1.0+ inference optimizations including dependency and parent inference.
 */
@DisplayName("InferenceStrategy")
class InferenceStrategyTest {

    private InferenceStrategy strategy;
    private SAXBuilder saxBuilder;

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

            UpgradeContext context = createMockContext(options);

            assertTrue(strategy.isApplicable(context), "Strategy should be applicable when --infer 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.infer()).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 when no specific options are provided");
        }

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

            UpgradeContext context = createMockContext(options);

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

    @Nested
    @DisplayName("Dependency Inference")
    class DependencyInferenceTests {

        @Test
        @DisplayName("should remove dependency version for project artifact")
        void shouldRemoveDependencyVersionForProjectArtifact() throws Exception {
            String parentPomXml = PomBuilder.create()
                    .namespace("http://maven.apache.org/POM/4.1.0")
                    .modelVersion("4.1.0")
                    .groupId("com.example")
                    .artifactId("parent-project")
                    .version("1.0.0")
                    .packaging("pom")
                    .build();

            String moduleAPomXml = PomBuilder.create()
                    .namespace("http://maven.apache.org/POM/4.1.0")
                    .modelVersion("4.1.0")
                    .parent("com.example", "parent-project", "1.0.0")
                    .artifactId("module-a")
                    .build();

            String moduleBPomXml =
                    """
                <?xml version="1.0" encoding="UTF-8"?>
                <project xmlns="http://maven.apache.org/POM/4.1.0">
                    <parent>
                        <groupId>com.example</groupId>
                        <artifactId>parent-project</artifactId>
                        <version>1.0.0</version>
                    </parent>
                    <artifactId>module-b</artifactId>
                    <dependencies>
                        <dependency>
                            <groupId>com.example</groupId>
                            <artifactId>module-a</artifactId>
                            <version>1.0.0</version>
                        </dependency>
                    </dependencies>
                </project>
                """;

            Document parentDoc = saxBuilder.build(new StringReader(parentPomXml));
            Document moduleADoc = saxBuilder.build(new StringReader(moduleAPomXml));
            Document moduleBDoc = saxBuilder.build(new StringReader(moduleBPomXml));

            Map<Path, Document> pomMap = new HashMap<>();
            pomMap.put(Paths.get("project", "pom.xml"), parentDoc);
            pomMap.put(Paths.get("project", "module-a", "pom.xml"), moduleADoc);
            pomMap.put(Paths.get("project", "module-b", "pom.xml"), moduleBDoc);

            Element moduleBRoot = moduleBDoc.getRootElement();
            Element dependencies = moduleBRoot.getChild("dependencies", moduleBRoot.getNamespace());
            Element dependency = dependencies.getChild("dependency", moduleBRoot.getNamespace());

            // Verify dependency elements exist before inference
            assertNotNull(dependency.getChild("groupId", moduleBRoot.getNamespace()));
            assertNotNull(dependency.getChild("artifactId", moduleBRoot.getNamespace()));
            assertNotNull(dependency.getChild("version", moduleBRoot.getNamespace()));

            // Apply dependency inference
            UpgradeContext context = createMockContext();
            strategy.apply(context, pomMap);

            // Verify version was removed (can be inferred from project)
            assertNull(dependency.getChild("version", moduleBRoot.getNamespace()));
            // groupId should also be removed (can be inferred from project)
            assertNull(dependency.getChild("groupId", moduleBRoot.getNamespace()));
            // artifactId should remain (always required)
            assertNotNull(dependency.getChild("artifactId", moduleBRoot.getNamespace()));
        }

        @Test
        @DisplayName("should keep dependency version for external artifact")
        void shouldKeepDependencyVersionForExternalArtifact() throws Exception {
            String modulePomXml =
                    """
                <?xml version="1.0" encoding="UTF-8"?>
                <project xmlns="http://maven.apache.org/POM/4.1.0">
                    <groupId>com.example</groupId>
                    <artifactId>my-module</artifactId>
                    <version>1.0.0</version>
                    <dependencies>
                        <dependency>
                            <groupId>org.apache.commons</groupId>
                            <artifactId>commons-lang3</artifactId>
                            <version>3.12.0</version>
                        </dependency>
                    </dependencies>
                </project>
                """;

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

            Element moduleRoot = moduleDoc.getRootElement();
            Element dependencies = moduleRoot.getChild("dependencies", moduleRoot.getNamespace());
            Element dependency = dependencies.getChild("dependency", moduleRoot.getNamespace());

            // Apply dependency inference
            UpgradeContext context = createMockContext();
            strategy.apply(context, pomMap);

            // Verify all dependency elements remain (external dependency)
            assertNotNull(dependency.getChild("groupId", moduleRoot.getNamespace()));
            assertNotNull(dependency.getChild("artifactId", moduleRoot.getNamespace()));
            assertNotNull(dependency.getChild("version", moduleRoot.getNamespace()));
        }

        @Test
        @DisplayName("should keep dependency version when version mismatch")
        void shouldKeepDependencyVersionWhenVersionMismatch() throws Exception {
            String moduleAPomXml = PomBuilder.create()
                    .namespace("http://maven.apache.org/POM/4.1.0")
                    .modelVersion("4.1.0")
                    .groupId("com.example")
                    .artifactId("module-a")
                    .version("1.0.0")
                    .build();

            String moduleBPomXml = PomBuilder.create()
                    .namespace("http://maven.apache.org/POM/4.1.0")
                    .modelVersion("4.1.0")
                    .groupId("com.example")
                    .artifactId("module-b")
                    .version("1.0.0")
                    .dependency("com.example", "module-a", "0.9.0")
                    .build();

            Document moduleADoc = saxBuilder.build(new StringReader(moduleAPomXml));
            Document moduleBDoc = saxBuilder.build(new StringReader(moduleBPomXml));

            Map<Path, Document> pomMap = new HashMap<>();
            pomMap.put(Paths.get("project", "module-a", "pom.xml"), moduleADoc);
            pomMap.put(Paths.get("project", "module-b", "pom.xml"), moduleBDoc);

            Element moduleBRoot = moduleBDoc.getRootElement();
            Element dependencies = moduleBRoot.getChild("dependencies", moduleBRoot.getNamespace());
            Element dependency = dependencies.getChild("dependency", moduleBRoot.getNamespace());

            // Apply dependency inference
            UpgradeContext context = createMockContext();
            strategy.apply(context, pomMap);

            // Verify correct behavior when version doesn't match:
            // - groupId should be removed (can be inferred from project regardless of version)
            // - version should remain (doesn't match project version, so can't be inferred)
            // - artifactId should remain (always required)
            assertNull(dependency.getChild("groupId", moduleBRoot.getNamespace()));
            assertNotNull(dependency.getChild("artifactId", moduleBRoot.getNamespace()));
            assertNotNull(dependency.getChild("version", moduleBRoot.getNamespace()));
        }

        @Test
        @DisplayName("should handle plugin dependencies")
        void shouldHandlePluginDependencies() throws Exception {
            String moduleAPomXml =
                    """
                <?xml version="1.0" encoding="UTF-8"?>
                <project xmlns="http://maven.apache.org/POM/4.1.0">
                    <groupId>com.example</groupId>
                    <artifactId>module-a</artifactId>
                    <version>1.0.0</version>
                </project>
                """;

            String moduleBPomXml =
                    """
                <?xml version="1.0" encoding="UTF-8"?>
                <project xmlns="http://maven.apache.org/POM/4.1.0">
                    <groupId>com.example</groupId>
                    <artifactId>module-b</artifactId>
                    <version>1.0.0</version>
                    <build>
                        <plugins>
                            <plugin>
                                <groupId>org.apache.maven.plugins</groupId>
                                <artifactId>maven-compiler-plugin</artifactId>
                                <dependencies>
                                    <dependency>
                                        <groupId>com.example</groupId>
                                        <artifactId>module-a</artifactId>
                                        <version>1.0.0</version>
                                    </dependency>
                                </dependencies>
                            </plugin>
                        </plugins>
                    </build>
                </project>
                """;

            Document moduleADoc = saxBuilder.build(new StringReader(moduleAPomXml));
            Document moduleBDoc = saxBuilder.build(new StringReader(moduleBPomXml));

            Map<Path, Document> pomMap = new HashMap<>();
            pomMap.put(Paths.get("project", "module-a", "pom.xml"), moduleADoc);
            pomMap.put(Paths.get("project", "module-b", "pom.xml"), moduleBDoc);

            Element moduleBRoot = moduleBDoc.getRootElement();
            Element build = moduleBRoot.getChild("build", moduleBRoot.getNamespace());
            Element plugins = build.getChild("plugins", moduleBRoot.getNamespace());
            Element plugin = plugins.getChild("plugin", moduleBRoot.getNamespace());
            Element dependencies = plugin.getChild("dependencies", moduleBRoot.getNamespace());
            Element dependency = dependencies.getChild("dependency", moduleBRoot.getNamespace());

            // Apply dependency inference
            UpgradeContext context = createMockContext();
            strategy.apply(context, pomMap);

            // Verify version and groupId were removed from plugin dependency
            assertNull(dependency.getChild("version", moduleBRoot.getNamespace()));
            assertNull(dependency.getChild("groupId", moduleBRoot.getNamespace()));
            assertNotNull(dependency.getChild("artifactId", moduleBRoot.getNamespace()));
        }
    }

    @Nested
    @DisplayName("Parent Inference")
    class ParentInferenceTests {

        @Test
        @DisplayName("should remove parent groupId when child doesn't have explicit groupId")
        void shouldRemoveParentGroupIdWhenChildDoesntHaveExplicitGroupId() throws Exception {
            String parentPomXml =
                    """
                <?xml version="1.0" encoding="UTF-8"?>
                <project xmlns="http://maven.apache.org/POM/4.1.0">
                    <modelVersion>4.1.0</modelVersion>
                    <groupId>com.example</groupId>
                    <artifactId>parent-project</artifactId>
                    <version>1.0.0</version>
                </project>
                """;

            String childPomXml =
                    """
                <?xml version="1.0" encoding="UTF-8"?>
                <project xmlns="http://maven.apache.org/POM/4.1.0">
                    <modelVersion>4.1.0</modelVersion>
                    <parent>
                        <groupId>com.example</groupId>
                        <artifactId>parent-project</artifactId>
                        <version>1.0.0</version>
                        <relativePath>../pom.xml</relativePath>
                    </parent>
                    <artifactId>child-project</artifactId>
                    <!-- No explicit groupId - will inherit from parent -->
                    <!-- No explicit version - will inherit from parent -->
                </project>
                """;

            Document parentDoc = saxBuilder.build(new StringReader(parentPomXml));
            Document childDoc = saxBuilder.build(new StringReader(childPomXml));

            Map<Path, Document> pomMap = new HashMap<>();
            pomMap.put(Paths.get("project", "pom.xml"), parentDoc);
            pomMap.put(Paths.get("project", "child", "pom.xml"), childDoc);

            Element childRoot = childDoc.getRootElement();
            Element parentElement = childRoot.getChild("parent", childRoot.getNamespace());

            // Verify parent elements exist before inference
            assertNotNull(parentElement.getChild("groupId", childRoot.getNamespace()));
            assertNotNull(parentElement.getChild("artifactId", childRoot.getNamespace()));
            assertNotNull(parentElement.getChild("version", childRoot.getNamespace()));

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

            // Verify parent groupId and version were removed (since child doesn't have explicit ones)
            assertNull(parentElement.getChild("groupId", childRoot.getNamespace()));
            assertNull(parentElement.getChild("version", childRoot.getNamespace()));
            // artifactId should also be removed since parent POM is in pomMap
            assertNull(parentElement.getChild("artifactId", childRoot.getNamespace()));
        }

        @Test
        @DisplayName("should keep parent groupId when child has explicit groupId")
        void shouldKeepParentGroupIdWhenChildHasExplicitGroupId() throws Exception {
            String parentPomXml =
                    """
                <?xml version="1.0" encoding="UTF-8"?>
                <project xmlns="http://maven.apache.org/POM/4.1.0">
                    <modelVersion>4.1.0</modelVersion>
                    <groupId>com.example</groupId>
                    <artifactId>parent-project</artifactId>
                    <version>1.0.0</version>
                </project>
                """;

            String childPomXml =
                    """
                <?xml version="1.0" encoding="UTF-8"?>
                <project xmlns="http://maven.apache.org/POM/4.1.0">
                    <modelVersion>4.1.0</modelVersion>
                    <parent>
                        <groupId>com.example</groupId>
                        <artifactId>parent-project</artifactId>
                        <version>1.0.0</version>
                        <relativePath>../pom.xml</relativePath>
                    </parent>
                    <groupId>com.example.child</groupId>
                    <artifactId>child-project</artifactId>
                    <version>2.0.0</version>
                </project>
                """;

            Document parentDoc = saxBuilder.build(new StringReader(parentPomXml));
            Document childDoc = saxBuilder.build(new StringReader(childPomXml));

            Map<Path, Document> pomMap = new HashMap<>();
            pomMap.put(Paths.get("project", "pom.xml"), parentDoc);
            pomMap.put(Paths.get("project", "child", "pom.xml"), childDoc);

            Element childRoot = childDoc.getRootElement();
            Element parentElement = childRoot.getChild("parent", childRoot.getNamespace());

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

            // Verify parent elements are kept (since child has explicit values)
            assertNotNull(parentElement.getChild("groupId", childRoot.getNamespace()));
            assertNotNull(parentElement.getChild("version", childRoot.getNamespace()));
            // artifactId should still be removed since parent POM is in pomMap
            assertNull(parentElement.getChild("artifactId", childRoot.getNamespace()));
        }

        @Test
        @DisplayName("should not trim parent elements when parent is external")
        void shouldNotTrimParentElementsWhenParentIsExternal() throws Exception {
            String childPomXml =
                    """
                <?xml version="1.0" encoding="UTF-8"?>
                <project xmlns="http://maven.apache.org/POM/4.1.0">
                    <modelVersion>4.1.0</modelVersion>
                    <parent>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-parent</artifactId>
                        <version>3.0.0</version>
                        <relativePath/>
                    </parent>
                    <artifactId>my-spring-app</artifactId>
                    <!-- No explicit groupId or version - would inherit from parent -->
                </project>
                """;

            Document childDoc = saxBuilder.build(new StringReader(childPomXml));

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

            Element childRoot = childDoc.getRootElement();
            Element parentElement = childRoot.getChild("parent", childRoot.getNamespace());

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

            // Verify correct behavior for external parent:
            // - groupId should NOT be removed (external parents need groupId to be located)
            // - artifactId should NOT be removed (external parents need artifactId to be located)
            // - version should NOT be removed (external parents need version to be located)
            // This prevents the "parent.groupId is missing" error reported in issue #7934
            assertNotNull(parentElement.getChild("groupId", childRoot.getNamespace()));
            assertNotNull(parentElement.getChild("artifactId", childRoot.getNamespace()));
            assertNotNull(parentElement.getChild("version", childRoot.getNamespace()));
        }

        @Test
        @DisplayName("should trim parent elements when parent is in reactor")
        void shouldTrimParentElementsWhenParentIsInReactor() throws Exception {
            // Create parent POM
            String parentPomXml =
                    """
                <?xml version="1.0" encoding="UTF-8"?>
                <project xmlns="http://maven.apache.org/POM/4.1.0">
                    <modelVersion>4.1.0</modelVersion>
                    <groupId>com.example</groupId>
                    <artifactId>parent-project</artifactId>
                    <version>1.0.0</version>
                    <packaging>pom</packaging>
                </project>
                """;

            // Create child POM that references the parent
            String childPomXml =
                    """
                <?xml version="1.0" encoding="UTF-8"?>
                <project xmlns="http://maven.apache.org/POM/4.1.0">
                    <modelVersion>4.1.0</modelVersion>
                    <parent>
                        <groupId>com.example</groupId>
                        <artifactId>parent-project</artifactId>
                        <version>1.0.0</version>
                    </parent>
                    <artifactId>child-project</artifactId>
                    <!-- No explicit groupId or version - would inherit from parent -->
                </project>
                """;

            Document parentDoc = saxBuilder.build(new StringReader(parentPomXml));
            Document childDoc = saxBuilder.build(new StringReader(childPomXml));

            // Both POMs are in the reactor
            Map<Path, Document> pomMap = Map.of(
                    Paths.get("pom.xml"), parentDoc,
                    Paths.get("child", "pom.xml"), childDoc);

            Element childRoot = childDoc.getRootElement();
            Element parentElement = childRoot.getChild("parent", childRoot.getNamespace());

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

            // Verify correct behavior for reactor parent:
            // - groupId should be removed (child has no explicit groupId, parent is in reactor)
            // - artifactId should be removed (can be inferred from relativePath)
            // - version should be removed (child has no explicit version, parent is in reactor)
            assertNull(parentElement.getChild("groupId", childRoot.getNamespace()));
            assertNull(parentElement.getChild("artifactId", childRoot.getNamespace()));
            assertNull(parentElement.getChild("version", childRoot.getNamespace()));
        }
    }

    @Nested
    @DisplayName("Maven 4.0.0 Limited Inference")
    class Maven400LimitedInferenceTests {

        @Test
        @DisplayName("should remove child groupId and version when they match parent in 4.0.0")
        void shouldRemoveChildGroupIdAndVersionWhenTheyMatchParentIn400() throws Exception {
            String parentPomXml =
                    """
                <?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>parent-project</artifactId>
                    <version>1.0.0</version>
                    <packaging>pom</packaging>
                </project>
                """;

            String childPomXml =
                    """
                <?xml version="1.0" encoding="UTF-8"?>
                <project xmlns="http://maven.apache.org/POM/4.0.0">
                    <modelVersion>4.0.0</modelVersion>
                    <parent>
                        <groupId>com.example</groupId>
                        <artifactId>parent-project</artifactId>
                        <version>1.0.0</version>
                        <relativePath>../pom.xml</relativePath>
                    </parent>
                    <groupId>com.example</groupId>
                    <artifactId>child-project</artifactId>
                    <version>1.0.0</version>
                    <!-- Child groupId and version match parent - can be inferred -->
                </project>
                """;

            Document parentDoc = saxBuilder.build(new StringReader(parentPomXml));
            Document childDoc = saxBuilder.build(new StringReader(childPomXml));

            Map<Path, Document> pomMap = new HashMap<>();
            pomMap.put(Paths.get("project", "pom.xml"), parentDoc);
            pomMap.put(Paths.get("project", "child", "pom.xml"), childDoc);

            Element childRoot = childDoc.getRootElement();
            Element parentElement = childRoot.getChild("parent", childRoot.getNamespace());

            // Verify child and parent elements exist before inference
            assertNotNull(childRoot.getChild("groupId", childRoot.getNamespace()));
            assertNotNull(childRoot.getChild("version", childRoot.getNamespace()));
            assertNotNull(parentElement.getChild("groupId", childRoot.getNamespace()));
            assertNotNull(parentElement.getChild("artifactId", childRoot.getNamespace()));
            assertNotNull(parentElement.getChild("version", childRoot.getNamespace()));

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

            // Verify child groupId and version were removed (Maven 4.0.0 can infer these from parent)
            assertNull(childRoot.getChild("groupId", childRoot.getNamespace()));
            assertNull(childRoot.getChild("version", childRoot.getNamespace()));
            // Child artifactId should remain (always required)
            assertNotNull(childRoot.getChild("artifactId", childRoot.getNamespace()));
            // Parent elements should all remain (no relativePath inference in 4.0.0)
            assertNotNull(parentElement.getChild("groupId", childRoot.getNamespace()));
            assertNotNull(parentElement.getChild("artifactId", childRoot.getNamespace()));
            assertNotNull(parentElement.getChild("version", childRoot.getNamespace()));
        }

        @Test
        @DisplayName("should keep child groupId when it differs from parent in 4.0.0")
        void shouldKeepChildGroupIdWhenItDiffersFromParentIn400() throws Exception {
            String parentPomXml =
                    """
                <?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>parent-project</artifactId>
                    <version>1.0.0</version>
                    <packaging>pom</packaging>
                </project>
                """;

            String childPomXml =
                    """
                <?xml version="1.0" encoding="UTF-8"?>
                <project xmlns="http://maven.apache.org/POM/4.0.0">
                    <modelVersion>4.0.0</modelVersion>
                    <parent>
                        <groupId>com.example</groupId>
                        <artifactId>parent-project</artifactId>
                        <version>1.0.0</version>
                        <relativePath>../pom.xml</relativePath>
                    </parent>
                    <groupId>com.example.child</groupId>
                    <artifactId>child-project</artifactId>
                    <version>2.0.0</version>
                </project>
                """;

            Document parentDoc = saxBuilder.build(new StringReader(parentPomXml));
            Document childDoc = saxBuilder.build(new StringReader(childPomXml));

            Map<Path, Document> pomMap = new HashMap<>();
            pomMap.put(Paths.get("project", "pom.xml"), parentDoc);
            pomMap.put(Paths.get("project", "child", "pom.xml"), childDoc);

            Element childRoot = childDoc.getRootElement();
            Element parentElement = childRoot.getChild("parent", childRoot.getNamespace());

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

            // Verify child elements are kept (since they differ from parent)
            assertNotNull(childRoot.getChild("groupId", childRoot.getNamespace()));
            assertNotNull(childRoot.getChild("version", childRoot.getNamespace()));
            assertNotNull(childRoot.getChild("artifactId", childRoot.getNamespace()));
            // Parent elements should all remain (no relativePath inference in 4.0.0)
            assertNotNull(parentElement.getChild("groupId", childRoot.getNamespace()));
            assertNotNull(parentElement.getChild("artifactId", childRoot.getNamespace()));
            assertNotNull(parentElement.getChild("version", childRoot.getNamespace()));
        }

        @Test
        @DisplayName("should handle partial inheritance in 4.0.0")
        void shouldHandlePartialInheritanceIn400() throws Exception {
            String parentPomXml =
                    """
                <?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>parent-project</artifactId>
                    <version>1.0.0</version>
                    <packaging>pom</packaging>
                </project>
                """;

            String childPomXml =
                    """
                <?xml version="1.0" encoding="UTF-8"?>
                <project xmlns="http://maven.apache.org/POM/4.0.0">
                    <modelVersion>4.0.0</modelVersion>
                    <parent>
                        <groupId>com.example</groupId>
                        <artifactId>parent-project</artifactId>
                        <version>1.0.0</version>
                        <relativePath>../pom.xml</relativePath>
                    </parent>
                    <groupId>com.example</groupId>
                    <artifactId>child-project</artifactId>
                    <version>2.0.0</version>
                    <!-- Child groupId matches parent, but version differs -->
                </project>
                """;

            Document parentDoc = saxBuilder.build(new StringReader(parentPomXml));
            Document childDoc = saxBuilder.build(new StringReader(childPomXml));

            Map<Path, Document> pomMap = new HashMap<>();
            pomMap.put(Paths.get("project", "pom.xml"), parentDoc);
            pomMap.put(Paths.get("project", "child", "pom.xml"), childDoc);

            Element childRoot = childDoc.getRootElement();
            Element parentElement = childRoot.getChild("parent", childRoot.getNamespace());

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

            // Verify child groupId was removed (matches parent, can be inferred)
            assertNull(childRoot.getChild("groupId", childRoot.getNamespace()));
            // Verify child version was kept (differs from parent, cannot be inferred)
            assertNotNull(childRoot.getChild("version", childRoot.getNamespace()));
            // Verify child artifactId was kept (always required)
            assertNotNull(childRoot.getChild("artifactId", childRoot.getNamespace()));
            // Parent elements should all remain (no relativePath inference in 4.0.0)
            assertNotNull(parentElement.getChild("groupId", childRoot.getNamespace()));
            assertNotNull(parentElement.getChild("artifactId", childRoot.getNamespace()));
            assertNotNull(parentElement.getChild("version", childRoot.getNamespace()));
        }

        @Test
        @DisplayName("should not apply dependency inference to 4.0.0 models")
        void shouldNotApplyDependencyInferenceTo400Models() throws Exception {
            String moduleAPomXml = PomBuilder.create()
                    .namespace("http://maven.apache.org/POM/4.0.0")
                    .modelVersion("4.0.0")
                    .groupId("com.example")
                    .artifactId("module-a")
                    .version("1.0.0")
                    .build();

            String moduleBPomXml = PomBuilder.create()
                    .namespace("http://maven.apache.org/POM/4.0.0")
                    .modelVersion("4.0.0")
                    .groupId("com.example")
                    .artifactId("module-b")
                    .version("1.0.0")
                    .dependency("com.example", "module-a", "1.0.0")
                    .build();

            Document moduleADoc = saxBuilder.build(new StringReader(moduleAPomXml));
            Document moduleBDoc = saxBuilder.build(new StringReader(moduleBPomXml));

            Map<Path, Document> pomMap = new HashMap<>();
            pomMap.put(Paths.get("project", "module-a", "pom.xml"), moduleADoc);
            pomMap.put(Paths.get("project", "module-b", "pom.xml"), moduleBDoc);

            Element moduleBRoot = moduleBDoc.getRootElement();
            Element dependency = moduleBRoot
                    .getChild("dependencies", moduleBRoot.getNamespace())
                    .getChildren("dependency", moduleBRoot.getNamespace())
                    .get(0);

            // Verify dependency elements exist before inference
            assertNotNull(dependency.getChild("groupId", moduleBRoot.getNamespace()));
            assertNotNull(dependency.getChild("artifactId", moduleBRoot.getNamespace()));
            assertNotNull(dependency.getChild("version", moduleBRoot.getNamespace()));

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

            // Verify dependency inference was NOT applied (all elements should remain for 4.0.0)
            assertNotNull(dependency.getChild("groupId", moduleBRoot.getNamespace()));
            assertNotNull(dependency.getChild("artifactId", moduleBRoot.getNamespace()));
            assertNotNull(dependency.getChild("version", moduleBRoot.getNamespace()));
        }
    }

    @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("infer"), "Description should mention inference");
        }
    }
}