DefaultProfileSelectorTest.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.impl.model;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.apache.maven.api.model.Activation;
import org.apache.maven.api.model.ActivationFile;
import org.apache.maven.api.model.ActivationProperty;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.model.Profile;
import org.apache.maven.impl.DefaultModelVersionParser;
import org.apache.maven.impl.model.profile.FileProfileActivator;
import org.apache.maven.impl.model.profile.PropertyProfileActivator;
import org.apache.maven.impl.model.profile.SimpleProblemCollector;
import org.apache.maven.impl.model.rootlocator.DefaultRootLocator;
import org.eclipse.aether.util.version.GenericVersionScheme;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * Tests {@link DefaultProfileSelector} with focus on cascading activation behavior.
 */
public class DefaultProfileSelectorTest {

    private DefaultProfileSelector selector;
    private SimpleProblemCollector problems;

    @BeforeEach
    void setUp() {
        selector = new DefaultProfileSelector();
        selector.addProfileActivator(
                new PropertyProfileActivator(new DefaultModelVersionParser(new GenericVersionScheme())));
        selector.addProfileActivator(new FileProfileActivator());
        problems = new SimpleProblemCollector();
    }

    @Test
    void testNonCascadingActivation() {
        // Create profiles with property-based activation
        Profile profile1 = createProfile("profile1", "prop1", "value1", Profile.SOURCE_POM);
        Profile profile2 = createProfile("profile2", "prop2", "value2", Profile.SOURCE_POM);

        List<Profile> profiles = Arrays.asList(profile1, profile2);

        // Create context with prop1 set
        DefaultProfileActivationContext context =
                new DefaultProfileActivationContext(new DefaultRootLocator(), new DefaultInterpolator());
        context.setModel(Model.newInstance());
        context.setSystemProperties(Map.of("prop1", "value1"));

        // Test cascading mode (current implementation only supports cascading)
        List<Profile> activeProfiles = selector.getActiveProfiles(profiles, context, problems);

        assertEquals(1, activeProfiles.size());
        assertEquals("profile1", activeProfiles.get(0).getId());
        assertTrue(problems.getErrors().isEmpty());
    }

    @Test
    void testCascadingActivation() {
        // Create profiles where one activates another through properties
        Profile profile1 = createProfileWithProperties(
                "profile1", "prop1", "value1", Map.of("prop2", "value2"), Profile.SOURCE_POM);
        Profile profile2 = createProfile("profile2", "prop2", "value2", Profile.SOURCE_POM);

        List<Profile> profiles = Arrays.asList(profile1, profile2);

        // Create context with prop1 set (should activate profile1, which sets prop2, which activates profile2)
        DefaultProfileActivationContext context =
                new DefaultProfileActivationContext(new DefaultRootLocator(), new DefaultInterpolator());
        context.setSystemProperties(Map.of("prop1", "value1"));
        context.setModel(Model.newInstance()); // Set a model for property injection

        // Test cascading mode
        List<Profile> activeProfiles = selector.getActiveProfiles(profiles, context, problems);

        assertEquals(2, activeProfiles.size());
        assertTrue(activeProfiles.stream().anyMatch(p -> "profile1".equals(p.getId())));
        assertTrue(activeProfiles.stream().anyMatch(p -> "profile2".equals(p.getId())));
        assertTrue(problems.getErrors().isEmpty());
    }

    @Test
    void testCascadingVsNonCascadingDifference() {
        // Create profiles where cascading would activate more profiles
        Profile profile1 = createProfileWithProperties(
                "profile1", "prop1", "value1", Map.of("prop2", "value2"), Profile.SOURCE_POM);
        Profile profile2 = createProfile("profile2", "prop2", "value2", Profile.SOURCE_POM);

        List<Profile> profiles = Arrays.asList(profile1, profile2);

        DefaultProfileActivationContext context =
                new DefaultProfileActivationContext(new DefaultRootLocator(), new DefaultInterpolator());
        context.setSystemProperties(Map.of("prop1", "value1"));
        context.setModel(Model.newInstance()); // Set a model for property injection

        // Cascading should activate both profile1 and profile2
        List<Profile> cascading = selector.getActiveProfiles(profiles, context, problems);
        assertEquals(2, cascading.size());
    }

    @Test
    void testActiveByDefaultProfiles() {
        Profile defaultProfile = createActiveByDefaultProfile("default-profile", Profile.SOURCE_POM);
        Profile conditionalProfile = createProfile("conditional", "prop1", "value1", Profile.SOURCE_POM);

        List<Profile> profiles = Arrays.asList(defaultProfile, conditionalProfile);

        DefaultProfileActivationContext context =
                new DefaultProfileActivationContext(new DefaultRootLocator(), new DefaultInterpolator());
        context.setModel(Model.newInstance());

        // Should activate default profile when no conditions are met
        List<Profile> activeProfiles = selector.getActiveProfiles(profiles, context, problems);
        assertEquals(1, activeProfiles.size());
        assertEquals("default-profile", activeProfiles.get(0).getId());

        // Should not activate default profile when conditional profile is active
        context.setSystemProperties(Map.of("prop1", "value1"));
        activeProfiles = selector.getActiveProfiles(profiles, context, problems);
        assertEquals(1, activeProfiles.size());
        assertEquals("conditional", activeProfiles.get(0).getId());
    }

    @Test
    void testMixedSourceProfiles() {
        Profile pomProfile = createProfile("pom-profile", "prop1", "value1", Profile.SOURCE_POM);
        Profile settingsProfile = createProfile("settings-profile", "prop2", "value2", Profile.SOURCE_SETTINGS);

        List<Profile> profiles = Arrays.asList(pomProfile, settingsProfile);

        DefaultProfileActivationContext context =
                new DefaultProfileActivationContext(new DefaultRootLocator(), new DefaultInterpolator());
        context.setModel(Model.newInstance());
        context.setSystemProperties(Map.of("prop1", "value1", "prop2", "value2"));

        List<Profile> activeProfiles = selector.getActiveProfiles(profiles, context, problems);
        assertEquals(2, activeProfiles.size());

        // Settings profiles should come after POM profiles in the result
        assertEquals("pom-profile", activeProfiles.get(0).getId());
        assertEquals("settings-profile", activeProfiles.get(1).getId());
    }

    @Test
    void testEmptyProfilesList() {
        List<Profile> profiles = Collections.emptyList();
        DefaultProfileActivationContext context =
                new DefaultProfileActivationContext(new DefaultRootLocator(), new DefaultInterpolator());
        context.setModel(Model.newInstance());

        List<Profile> activeProfiles = selector.getActiveProfiles(profiles, context, problems);
        assertTrue(activeProfiles.isEmpty());
    }

    @Test
    void testExplicitlyActivatedProfiles() {
        Profile profile1 = createProfile("profile1", "nonexistent", "value", Profile.SOURCE_POM);
        Profile profile2 = createProfile("profile2", "prop2", "value2", Profile.SOURCE_POM);

        List<Profile> profiles = Arrays.asList(profile1, profile2);

        DefaultProfileActivationContext context = new DefaultProfileActivationContext(
                new DefaultRootLocator(),
                new DefaultInterpolator(),
                List.of("profile1"),
                List.of(),
                Map.of("prop2", "value2"),
                Map.of(),
                Model.newInstance());

        List<Profile> activeProfiles = selector.getActiveProfiles(profiles, context, problems);
        assertEquals(2, activeProfiles.size());
        assertTrue(activeProfiles.stream().anyMatch(p -> "profile1".equals(p.getId())));
        assertTrue(activeProfiles.stream().anyMatch(p -> "profile2".equals(p.getId())));
    }

    @Test
    void testCascadingActivationChain() {
        // Create a chain of profiles: profile1 -> profile2 -> profile3
        Profile profile1 = createProfileWithProperties(
                "profile1", "prop1", "value1", Map.of("prop2", "value2"), Profile.SOURCE_POM);
        Profile profile2 = createProfileWithProperties(
                "profile2", "prop2", "value2", Map.of("prop3", "value3"), Profile.SOURCE_POM);
        Profile profile3 = createProfile("profile3", "prop3", "value3", Profile.SOURCE_POM);

        List<Profile> profiles = Arrays.asList(profile1, profile2, profile3);

        // Create context with prop1 set
        DefaultProfileActivationContext context =
                new DefaultProfileActivationContext(new DefaultRootLocator(), new DefaultInterpolator());
        context.setSystemProperties(Map.of("prop1", "value1"));
        context.setModel(Model.newInstance());

        // Test cascading mode
        List<Profile> activeProfiles = selector.getActiveProfiles(profiles, context, problems);

        // All three profiles should be activated through cascading
        assertEquals(3, activeProfiles.size());
        assertTrue(activeProfiles.stream().anyMatch(p -> "profile1".equals(p.getId())));
        assertTrue(activeProfiles.stream().anyMatch(p -> "profile2".equals(p.getId())));
        assertTrue(activeProfiles.stream().anyMatch(p -> "profile3".equals(p.getId())));
        assertTrue(problems.getErrors().isEmpty());
    }

    @Test
    void testCascadingStopCondition() {
        // Test that cascading stops when no more profiles can be activated
        Profile profile1 = createProfileWithProperties(
                "profile1", "prop1", "value1", Map.of("prop2", "value2"), Profile.SOURCE_POM);
        Profile profile2 = createProfileWithProperties(
                "profile2", "prop2", "value2", Map.of("prop3", "value3"), Profile.SOURCE_POM);
        // profile3 requires prop4 which is never set, so cascading should stop
        Profile profile3 = createProfile("profile3", "prop4", "value4", Profile.SOURCE_POM);

        List<Profile> profiles = Arrays.asList(profile1, profile2, profile3);

        // Create context with prop1 set
        DefaultProfileActivationContext context =
                new DefaultProfileActivationContext(new DefaultRootLocator(), new DefaultInterpolator());
        context.setSystemProperties(Map.of("prop1", "value1"));
        context.setModel(Model.newInstance());

        // Test cascading mode
        List<Profile> activeProfiles = selector.getActiveProfiles(profiles, context, problems);

        // Only profile1 and profile2 should be activated, profile3 should not
        assertEquals(2, activeProfiles.size());
        assertTrue(activeProfiles.stream().anyMatch(p -> "profile1".equals(p.getId())));
        assertTrue(activeProfiles.stream().anyMatch(p -> "profile2".equals(p.getId())));
        assertTrue(activeProfiles.stream().noneMatch(p -> "profile3".equals(p.getId())));
        assertTrue(problems.getErrors().isEmpty());
    }

    @Test
    void testCascadingWithCircularDependency() {
        // Test that cascading handles circular dependencies gracefully
        Profile profile1 = createProfileWithProperties(
                "profile1", "prop1", "value1", Map.of("prop2", "value2"), Profile.SOURCE_POM);
        Profile profile2 = createProfileWithProperties(
                "profile2", "prop2", "value2", Map.of("prop1", "value1"), Profile.SOURCE_POM);

        List<Profile> profiles = Arrays.asList(profile1, profile2);

        // Create context with prop1 set
        DefaultProfileActivationContext context =
                new DefaultProfileActivationContext(new DefaultRootLocator(), new DefaultInterpolator());
        context.setSystemProperties(Map.of("prop1", "value1"));
        context.setModel(Model.newInstance());

        // Test cascading mode
        List<Profile> activeProfiles = selector.getActiveProfiles(profiles, context, problems);

        // Both profiles should be activated, but cascading should stop after first iteration
        assertEquals(2, activeProfiles.size());
        assertTrue(activeProfiles.stream().anyMatch(p -> "profile1".equals(p.getId())));
        assertTrue(activeProfiles.stream().anyMatch(p -> "profile2".equals(p.getId())));
        assertTrue(problems.getErrors().isEmpty());
    }

    @Test
    void testCascadingWithInactiveProfile() {
        // Create profiles where one would activate another, but the second is explicitly deactivated
        Profile profile1 = createProfileWithProperties(
                "profile1", "prop1", "value1", Map.of("prop2", "value2"), Profile.SOURCE_POM);
        Profile profile2 = createProfile("profile2", "prop2", "value2", Profile.SOURCE_POM);

        List<Profile> profiles = Arrays.asList(profile1, profile2);

        // Create context with prop1 set and profile2 explicitly deactivated
        DefaultProfileActivationContext context = new DefaultProfileActivationContext(
                new DefaultRootLocator(),
                new DefaultInterpolator(),
                List.of(),
                List.of("profile2"),
                Map.of("prop1", "value1"),
                Map.of(),
                Model.newInstance());

        // Test cascading mode
        List<Profile> activeProfiles = selector.getActiveProfiles(profiles, context, problems);

        // Only profile1 should be activated, profile2 should be deactivated despite cascading
        assertEquals(1, activeProfiles.size());
        assertTrue(activeProfiles.stream().anyMatch(p -> "profile1".equals(p.getId())));
        assertTrue(activeProfiles.stream().noneMatch(p -> "profile2".equals(p.getId())));
        assertTrue(problems.getErrors().isEmpty());
    }

    @Test
    void testActiveByDefaultDoesNotCascade() {
        // activeByDefault profiles must NOT inject cascading properties when suppressed
        // by a genuinely activated profile
        Profile defaultProfile = Profile.newBuilder()
                .id("default-on")
                .activation(Activation.newBuilder().activeByDefault(true).build())
                .properties(Map.of("prop2", "value2"))
                .build();
        defaultProfile.setSource(Profile.SOURCE_POM);

        Profile cascadeTarget = createProfile("cascade-target", "prop2", "value2", Profile.SOURCE_POM);
        Profile explicitProfile = createProfile("explicit", "trigger", "yes", Profile.SOURCE_POM);

        List<Profile> profiles = Arrays.asList(defaultProfile, cascadeTarget, explicitProfile);

        DefaultProfileActivationContext context =
                new DefaultProfileActivationContext(new DefaultRootLocator(), new DefaultInterpolator());
        context.setSystemProperties(Map.of("trigger", "yes"));
        context.setModel(Model.newInstance());

        List<Profile> activeProfiles = selector.getActiveProfiles(profiles, context, problems);

        // Only 'explicit' should be active. 'default-on' is suppressed because a non-default
        // POM profile activated. 'cascade-target' must NOT activate because the suppressed
        // default-on profile's properties should not cascade.
        assertEquals(1, activeProfiles.size());
        assertEquals("explicit", activeProfiles.get(0).getId());
        assertTrue(problems.getErrors().isEmpty());
    }

    @Test
    void testExplicitlyActivatedProfileDoesNotCascade() {
        // Reproduces the Windows CI failure: -Prun-its sets mavenHome as a profile property,
        // which must NOT cascade to activate maven-from-build (which activates on mavenHome).
        Profile runIts = createProfileWithProperties(
                "run-its", "nonexistent", "value", Map.of("mavenHome", "target/apache-maven"), Profile.SOURCE_POM);
        Profile mavenFromBuild = createProfile("maven-from-build", "mavenHome", null, Profile.SOURCE_POM);

        List<Profile> profiles = Arrays.asList(runIts, mavenFromBuild);

        // Explicitly activate run-its via -P (not via activator match)
        DefaultProfileActivationContext context = new DefaultProfileActivationContext(
                new DefaultRootLocator(),
                new DefaultInterpolator(),
                List.of("run-its"),
                List.of(),
                Map.of(),
                Map.of(),
                Model.newInstance());

        List<Profile> activeProfiles = selector.getActiveProfiles(profiles, context, problems);

        // Only run-its should be active; maven-from-build must NOT activate because
        // explicitly-activated profiles don't cascade their properties.
        assertEquals(1, activeProfiles.size());
        assertEquals("run-its", activeProfiles.get(0).getId());
        assertTrue(problems.getErrors().isEmpty());
    }

    @Test
    void testCascadingWithRecordImmutability() {
        // Test that profile records remain immutable during cascading
        Profile originalProfile1 = createProfileWithProperties(
                "profile1", "prop1", "value1", Map.of("prop2", "value2"), Profile.SOURCE_POM);
        Profile originalProfile2 = createProfile("profile2", "prop2", "value2", Profile.SOURCE_POM);

        List<Profile> profiles = Arrays.asList(originalProfile1, originalProfile2);

        // Create context with prop1 set
        DefaultProfileActivationContext context =
                new DefaultProfileActivationContext(new DefaultRootLocator(), new DefaultInterpolator());
        context.setSystemProperties(Map.of("prop1", "value1"));
        context.setModel(Model.newInstance());

        // Test cascading mode
        List<Profile> activeProfiles = selector.getActiveProfiles(profiles, context, problems);

        // Verify that original profiles are unchanged (immutable records)
        assertEquals("profile1", originalProfile1.getId());
        assertEquals("profile2", originalProfile2.getId());
        assertEquals(Map.of("prop2", "value2"), originalProfile1.getProperties());
        assertEquals(Map.of(), originalProfile2.getProperties());

        // Verify activation worked
        assertEquals(2, activeProfiles.size());
        assertTrue(problems.getErrors().isEmpty());
    }

    @Test
    void testFileExistenceCascade() {
        String existingDir = System.getProperty("java.io.tmpdir");

        // Profile 1: activates on trigger=yes, sets my.path to an existing directory
        Profile profile1 = createProfileWithProperties(
                "profile1", "trigger", "yes", Map.of("my.path", existingDir), Profile.SOURCE_POM);

        // Profile 2: activates on file existence using cascaded property
        Profile profile2 = Profile.newBuilder()
                .id("profile2")
                .activation(Activation.newBuilder()
                        .file(ActivationFile.newBuilder().exists("${my.path}").build())
                        .build())
                .build();
        profile2.setSource(Profile.SOURCE_POM);

        List<Profile> profiles = Arrays.asList(profile1, profile2);

        DefaultProfileActivationContext context =
                new DefaultProfileActivationContext(new DefaultRootLocator(), new DefaultInterpolator());
        context.setSystemProperties(Map.of("trigger", "yes"));
        context.setModel(Model.newInstance());

        List<Profile> activeProfiles = selector.getActiveProfiles(profiles, context, problems);

        // Both profiles should activate: profile1 via property match, profile2 via
        // file-existence check using the cascaded my.path property from profile1.
        assertEquals(2, activeProfiles.size());
        assertTrue(activeProfiles.stream().anyMatch(p -> "profile1".equals(p.getId())));
        assertTrue(activeProfiles.stream().anyMatch(p -> "profile2".equals(p.getId())));
        assertTrue(problems.getErrors().isEmpty());
    }

    // Helper methods for creating test profiles

    private Profile createProfile(String id, String propName, String propValue, String source) {
        Profile profile = Profile.newBuilder()
                .id(id)
                .activation(Activation.newBuilder()
                        .property(ActivationProperty.newBuilder()
                                .name(propName)
                                .value(propValue)
                                .build())
                        .build())
                .build();
        profile.setSource(source);
        return profile;
    }

    private Profile createProfileWithProperties(
            String id, String propName, String propValue, Map<String, String> profileProperties, String source) {
        Profile profile = Profile.newBuilder()
                .id(id)
                .activation(Activation.newBuilder()
                        .property(ActivationProperty.newBuilder()
                                .name(propName)
                                .value(propValue)
                                .build())
                        .build())
                .properties(profileProperties)
                .build();
        profile.setSource(source);
        return profile;
    }

    private Profile createActiveByDefaultProfile(String id, String source) {
        Profile profile = Profile.newBuilder()
                .id(id)
                .activation(Activation.newBuilder().activeByDefault(true).build())
                .build();
        profile.setSource(source);
        return profile;
    }
}