/*
 * 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.api.plugin.testing.stubs;

import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;

import org.apache.maven.api.Artifact;
import org.apache.maven.api.LocalRepository;
import org.apache.maven.api.ProducedArtifact;
import org.apache.maven.api.Project;
import org.apache.maven.api.RemoteRepository;
import org.apache.maven.api.Session;
import org.apache.maven.api.SessionData;
import org.apache.maven.api.annotations.Nonnull;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.model.Repository;
import org.apache.maven.api.services.ArtifactDeployer;
import org.apache.maven.api.services.ArtifactDeployerRequest;
import org.apache.maven.api.services.ArtifactFactory;
import org.apache.maven.api.services.ArtifactFactoryRequest;
import org.apache.maven.api.services.ArtifactInstaller;
import org.apache.maven.api.services.ArtifactInstallerRequest;
import org.apache.maven.api.services.ArtifactManager;
import org.apache.maven.api.services.LocalRepositoryManager;
import org.apache.maven.api.services.Lookup;
import org.apache.maven.api.services.ProjectBuilder;
import org.apache.maven.api.services.ProjectBuilderRequest;
import org.apache.maven.api.services.ProjectBuilderResult;
import org.apache.maven.api.services.ProjectManager;
import org.apache.maven.api.services.RepositoryFactory;
import org.apache.maven.api.services.VersionParser;
import org.apache.maven.api.services.xml.ModelXmlFactory;
import org.apache.maven.impl.DefaultModelVersionParser;
import org.apache.maven.impl.DefaultModelXmlFactory;
import org.apache.maven.impl.DefaultVersionParser;
import org.apache.maven.impl.InternalSession;
import org.apache.maven.model.v4.MavenStaxReader;
import org.eclipse.aether.util.version.GenericVersionScheme;
import org.mockito.ArgumentMatchers;
import org.mockito.quality.Strictness;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;

/**
 * A mock implementation of {@link InternalSession} for testing Maven plugins.
 * This class provides a comprehensive mock session that simulates the behavior
 * of a real Maven session, including repository management, artifact handling,
 * and project building capabilities.
 *
 * <p>The mock session includes pre-configured behaviors for:</p>
 * <ul>
 *   <li>Repository management (local and remote repositories)</li>
 *   <li>Artifact installation and deployment</li>
 *   <li>Project building and management</li>
 *   <li>Version parsing and handling</li>
 *   <li>Model XML processing</li>
 *   <li>Session data storage</li>
 * </ul>
 *
 * <p>Example usage in a test:</p>
 * <pre>
 * {@code
 * @Test
 * void testMojo() {
 *     // Create a mock session with a specific local repository
 *     InternalSession session = SessionMock.getMockSession("/path/to/local/repo");
 *
 *     // Use the session for testing
 *     MyMojo mojo = new MyMojo();
 *     mojo.setSession(session);
 *     mojo.execute();
 * }
 * }
 * </pre>
 *
 * <p>The mock session maintains internal state for:</p>
 * <ul>
 *   <li>Attached artifacts (via {@link ProjectManager})</li>
 *   <li>Artifact paths (via {@link ArtifactManager})</li>
 *   <li>System and user properties</li>
 *   <li>Session-scoped data (via {@link TestSessionData})</li>
 * </ul>
 *
 * <p>Most service implementations are mocked using Mockito, with pre-configured
 * behaviors that simulate typical Maven operations. Some services, like
 * {@link ModelXmlFactory} and {@link VersionParser}, use real implementations
 * to ensure correct handling of Maven models and versions.</p>
 *
 * @see InternalSession
 * @see LocalRepository
 * @see ProjectManager
 * @see ArtifactManager
 * @since 4.0.0
 */
public class SessionMock {

    public static InternalSession getMockSession(String localRepo) {
        LocalRepository localRepository = mock(LocalRepository.class);
        when(localRepository.getId()).thenReturn("local");
        when(localRepository.getPath()).thenReturn(Paths.get(localRepo));
        return getMockSession(localRepository);
    }

    @SuppressWarnings("checkstyle:MethodLength")
    public static InternalSession getMockSession(LocalRepository localRepository) {
        InternalSession session = mock(InternalSession.class);

        //
        // RepositoryFactory
        //
        RepositoryFactory repositoryFactory = mock(RepositoryFactory.class);
        when(session.createRemoteRepository(anyString(), anyString())).thenAnswer(iom -> {
            String id = iom.getArgument(0, String.class);
            String url = iom.getArgument(1, String.class);
            return session.getService(RepositoryFactory.class).createRemote(id, url);
        });
        when(session.createRemoteRepository(any()))
                .thenAnswer(iom -> repositoryFactory.createRemote(iom.getArgument(0, Repository.class)));
        when(repositoryFactory.createRemote(any(Repository.class))).thenAnswer(iom -> {
            Repository repository = iom.getArgument(0, Repository.class);
            return repositoryFactory.createRemote(repository.getId(), repository.getUrl());
        });
        when(repositoryFactory.createRemote(anyString(), anyString())).thenAnswer(iom -> {
            String id = iom.getArgument(0, String.class);
            String url = iom.getArgument(1, String.class);
            RemoteRepository remoteRepository =
                    mock(RemoteRepository.class, withSettings().strictness(Strictness.LENIENT));
            when(remoteRepository.getId()).thenReturn(id);
            when(remoteRepository.getUrl()).thenReturn(url);
            when(remoteRepository.getProtocol()).thenReturn(URI.create(url).getScheme());
            return remoteRepository;
        });
        when(session.getService(RepositoryFactory.class)).thenReturn(repositoryFactory);

        //
        // VersionParser
        //
        VersionParser versionParser =
                new DefaultVersionParser(new DefaultModelVersionParser(new GenericVersionScheme()));
        when(session.parseVersion(any()))
                .thenAnswer(iom -> versionParser.parseVersion(iom.getArgument(0, String.class)));
        when(session.getService(VersionParser.class)).thenReturn(versionParser);

        //
        // LocalRepositoryManager
        //
        LocalRepositoryManager localRepositoryManager = mock(LocalRepositoryManager.class);
        when(session.getPathForLocalArtifact(any(Artifact.class)))
                .then(iom -> localRepositoryManager.getPathForLocalArtifact(
                        session, session.getLocalRepository(), iom.getArgument(0, Artifact.class)));
        when(session.getPathForRemoteArtifact(any(), any()))
                .thenAnswer(iom -> localRepositoryManager.getPathForRemoteArtifact(
                        session,
                        session.getLocalRepository(),
                        iom.getArgument(0, RemoteRepository.class),
                        iom.getArgument(1, Artifact.class)));
        when(localRepositoryManager.getPathForLocalArtifact(any(), any(), any()))
                .thenAnswer(iom -> {
                    LocalRepository localRepo = iom.getArgument(1, LocalRepository.class);
                    Artifact artifact = iom.getArgument(2, Artifact.class);
                    return localRepo.getPath().resolve(getPathForArtifact(artifact));
                });
        when(session.getService(LocalRepositoryManager.class)).thenReturn(localRepositoryManager);

        //
        // ArtifactInstaller
        //
        ArtifactInstaller artifactInstaller = mock(ArtifactInstaller.class);
        doAnswer(iom -> {
                    artifactInstaller.install(
                            ArtifactInstallerRequest.build(session, iom.getArgument(0, Collection.class)));
                    return null;
                })
                .when(session)
                .installArtifacts(any(Collection.class));
        doAnswer(iom -> {
                    artifactInstaller.install(ArtifactInstallerRequest.build(
                            session, Arrays.asList(iom.getArgument(0, ProducedArtifact[].class))));
                    return null;
                })
                .when(session)
                .installArtifacts(any(ProducedArtifact[].class));
        doAnswer(iom -> {
                    artifactInstaller.install(ArtifactInstallerRequest.build(
                            iom.getArgument(0, Session.class), iom.getArgument(1, Collection.class)));
                    return null;
                })
                .when(artifactInstaller)
                .install(any(Session.class), ArgumentMatchers.<Collection<ProducedArtifact>>any());
        when(session.getService(ArtifactInstaller.class)).thenReturn(artifactInstaller);

        //
        // ArtifactDeployer
        //
        ArtifactDeployer artifactDeployer = mock(ArtifactDeployer.class);
        doAnswer(iom -> {
                    artifactDeployer.deploy(ArtifactDeployerRequest.build(
                            iom.getArgument(0, Session.class),
                            iom.getArgument(1, RemoteRepository.class),
                            Arrays.asList(iom.getArgument(2, ProducedArtifact[].class))));
                    return null;
                })
                .when(session)
                .deployArtifact(any(), any());
        doAnswer(iom -> {
                    artifactDeployer.deploy(ArtifactDeployerRequest.build(
                            iom.getArgument(0, Session.class),
                            iom.getArgument(1, RemoteRepository.class),
                            iom.getArgument(2, Collection.class)));
                    return null;
                })
                .when(artifactDeployer)
                .deploy(any(), any(), any());
        when(session.getService(ArtifactDeployer.class)).thenReturn(artifactDeployer);

        //
        // ArtifactManager
        //
        ArtifactManager artifactManager = mock(ArtifactManager.class);
        Map<Artifact, Path> paths = new HashMap<>();
        doAnswer(iom -> {
                    paths.put(iom.getArgument(0), iom.getArgument(1));
                    return null;
                })
                .when(artifactManager)
                .setPath(any(), any());
        doAnswer(iom -> Optional.ofNullable(paths.get(iom.getArgument(0, Artifact.class))))
                .when(artifactManager)
                .getPath(any());
        doAnswer(iom -> artifactManager.getPath(iom.getArgument(0, Artifact.class)))
                .when(session)
                .getArtifactPath(any());
        when(session.getService(ArtifactManager.class)).thenReturn(artifactManager);

        //
        // ProjectManager
        //
        ProjectManager projectManager = mock(ProjectManager.class);
        Map<Project, Collection<Artifact>> attachedArtifacts = new HashMap<>();
        doAnswer(iom -> {
                    Project project = iom.getArgument(1, Project.class);
                    String type = iom.getArgument(2, String.class);
                    Path path = iom.getArgument(3, Path.class);
                    ProducedArtifact artifact = session.createProducedArtifact(
                            project.getGroupId(), project.getArtifactId(), project.getVersion(), null, null, type);
                    artifactManager.setPath(artifact, path);
                    attachedArtifacts
                            .computeIfAbsent(project, p -> new ArrayList<>())
                            .add(artifact);
                    return null;
                })
                .when(projectManager)
                .attachArtifact(same(session), any(Project.class), any(), any());
        doAnswer(iom -> {
                    Project project = iom.getArgument(0, Project.class);
                    ProducedArtifact artifact = iom.getArgument(1, ProducedArtifact.class);
                    Path path = iom.getArgument(2, Path.class);
                    artifactManager.setPath(artifact, path);
                    attachedArtifacts
                            .computeIfAbsent(project, p -> new ArrayList<>())
                            .add(artifact);
                    return null;
                })
                .when(projectManager)
                .attachArtifact(any(Project.class), any(ProducedArtifact.class), any(Path.class));
        when(projectManager.getAttachedArtifacts(any()))
                .then(iom ->
                        attachedArtifacts.computeIfAbsent(iom.getArgument(0, Project.class), p -> new ArrayList<>()));
        when(projectManager.getAllArtifacts(any())).then(iom -> {
            Project project = iom.getArgument(0, Project.class);
            List<Artifact> result = new ArrayList<>();
            result.addAll(project.getArtifacts());
            result.addAll(attachedArtifacts.computeIfAbsent(project, p -> new ArrayList<>()));
            return result;
        });
        when(session.getService(ProjectManager.class)).thenReturn(projectManager);

        //
        // ArtifactFactory
        //
        ArtifactFactory artifactFactory = mock(ArtifactFactory.class);
        when(artifactFactory.create(any())).then(iom -> {
            ArtifactFactoryRequest request = iom.getArgument(0, ArtifactFactoryRequest.class);
            String classifier = request.getClassifier();
            String extension = request.getExtension();
            String type = request.getType();
            if (classifier == null) {
                classifier = "";
            }
            if (extension == null) {
                extension = type != null ? type : "";
            }
            return new ArtifactStub(
                    request.getGroupId(), request.getArtifactId(), classifier, request.getVersion(), extension);
        });
        when(artifactFactory.createProduced(any())).then(iom -> {
            ArtifactFactoryRequest request = iom.getArgument(0, ArtifactFactoryRequest.class);
            String classifier = request.getClassifier();
            String extension = request.getExtension();
            String type = request.getType();
            if (classifier == null) {
                classifier = "";
            }
            if (extension == null) {
                extension = type != null ? type : "";
            }
            return new ProducedArtifactStub(
                    request.getGroupId(), request.getArtifactId(), classifier, request.getVersion(), extension);
        });
        when(session.createArtifact(any(), any(), any(), any(), any(), any())).thenAnswer(iom -> {
            String groupId = iom.getArgument(0, String.class);
            String artifactId = iom.getArgument(1, String.class);
            String version = iom.getArgument(2, String.class);
            String classifier = iom.getArgument(3, String.class);
            String extension = iom.getArgument(4, String.class);
            String type = iom.getArgument(5, String.class);
            return session.getService(ArtifactFactory.class)
                    .create(ArtifactFactoryRequest.builder()
                            .session(session)
                            .groupId(groupId)
                            .artifactId(artifactId)
                            .version(version)
                            .classifier(classifier)
                            .extension(extension)
                            .type(type)
                            .build());
        });
        when(session.createArtifact(any(), any(), any(), any())).thenAnswer(iom -> {
            String groupId = iom.getArgument(0, String.class);
            String artifactId = iom.getArgument(1, String.class);
            String version = iom.getArgument(2, String.class);
            String extension = iom.getArgument(3, String.class);
            return session.getService(ArtifactFactory.class)
                    .create(ArtifactFactoryRequest.builder()
                            .session(session)
                            .groupId(groupId)
                            .artifactId(artifactId)
                            .version(version)
                            .extension(extension)
                            .build());
        });
        when(session.createProducedArtifact(any(), any(), any(), any(), any(), any()))
                .thenAnswer(iom -> {
                    String groupId = iom.getArgument(0, String.class);
                    String artifactId = iom.getArgument(1, String.class);
                    String version = iom.getArgument(2, String.class);
                    String classifier = iom.getArgument(3, String.class);
                    String extension = iom.getArgument(4, String.class);
                    String type = iom.getArgument(5, String.class);
                    return session.getService(ArtifactFactory.class)
                            .createProduced(ArtifactFactoryRequest.builder()
                                    .session(session)
                                    .groupId(groupId)
                                    .artifactId(artifactId)
                                    .version(version)
                                    .classifier(classifier)
                                    .extension(extension)
                                    .type(type)
                                    .build());
                });
        when(session.createProducedArtifact(any(), any(), any(), any())).thenAnswer(iom -> {
            String groupId = iom.getArgument(0, String.class);
            String artifactId = iom.getArgument(1, String.class);
            String version = iom.getArgument(2, String.class);
            String extension = iom.getArgument(3, String.class);
            return session.getService(ArtifactFactory.class)
                    .createProduced(ArtifactFactoryRequest.builder()
                            .session(session)
                            .groupId(groupId)
                            .artifactId(artifactId)
                            .version(version)
                            .extension(extension)
                            .build());
        });
        when(session.getService(ArtifactFactory.class)).thenReturn(artifactFactory);

        //
        // ProjectBuilder
        //
        ProjectBuilder projectBuilder = mock(ProjectBuilder.class);
        when(projectBuilder.build(any(ProjectBuilderRequest.class))).then(iom -> {
            ProjectBuilderRequest request = iom.getArgument(0, ProjectBuilderRequest.class);
            ProjectBuilderResult result = mock(ProjectBuilderResult.class);
            Model model =
                    new MavenStaxReader().read(request.getSource().orElseThrow().openStream());
            ProjectStub projectStub = new ProjectStub();
            projectStub.setModel(model);
            ProducedArtifactStub artifactStub = new ProducedArtifactStub(
                    model.getGroupId(), model.getArtifactId(), "", model.getVersion(), model.getPackaging());
            if (!"pom".equals(model.getPackaging())) {
                projectStub.setMainArtifact(artifactStub);
            }
            when(result.getProject()).thenReturn(Optional.of(projectStub));
            return result;
        });
        when(session.getService(ProjectBuilder.class)).thenReturn(projectBuilder);

        //
        // ModelXmlFactory
        //
        when(session.getService(ModelXmlFactory.class)).thenReturn(new DefaultModelXmlFactory());

        //
        // Lookup
        //
        when(session.getService(Lookup.class)).thenReturn(LookupStub.EMPTY);

        //
        // Other
        //
        Properties sysProps = new Properties();
        Properties usrProps = new Properties();
        doReturn(sysProps).when(session).getSystemProperties();
        doReturn(usrProps).when(session).getUserProperties();
        when(session.getLocalRepository()).thenReturn(localRepository);
        when(session.getData()).thenReturn(new TestSessionData());
        when(session.withLocalRepository(any()))
                .thenAnswer(iom -> getMockSession(iom.getArgument(0, LocalRepository.class)));

        return session;
    }

    static String getPathForArtifact(Artifact artifact) {
        StringBuilder path = new StringBuilder(128);
        path.append(artifact.getGroupId().replace('.', '/')).append('/');
        path.append(artifact.getArtifactId()).append('/');
        path.append(artifact.getVersion()).append('/');
        path.append(artifact.getArtifactId()).append('-');
        path.append(artifact.getVersion());
        if (!artifact.getClassifier().isEmpty()) {
            path.append('-').append(artifact.getClassifier());
        }
        if (!artifact.getExtension().isEmpty()) {
            path.append('.').append(artifact.getExtension());
        }
        return path.toString();
    }

    static class TestSessionData implements SessionData {
        private final Map<Key<?>, Object> map = new ConcurrentHashMap<>();

        @Override
        public <T> void set(@Nonnull Key<T> key, T value) {
            map.put(key, value);
        }

        @Override
        public <T> boolean replace(@Nonnull Key<T> key, T oldValue, T newValue) {
            return map.replace(key, oldValue, newValue);
        }

        @Override
        @SuppressWarnings("unchecked")
        public <T> T get(@Nonnull Key<T> key) {
            return (T) map.get(key);
        }

        @Override
        @SuppressWarnings("unchecked")
        public <T> T computeIfAbsent(@Nonnull Key<T> key, @Nonnull Supplier<T> supplier) {
            return (T) map.computeIfAbsent(key, k -> supplier.get());
        }
    }
}
