/*
 * 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.eclipse.aether.internal.impl.filter;

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.impl.MetadataResolver;
import org.eclipse.aether.impl.RemoteRepositoryManager;
import org.eclipse.aether.internal.impl.DefaultArtifactPredicateFactory;
import org.eclipse.aether.internal.impl.DefaultRepositoryKeyFunctionFactory;
import org.eclipse.aether.internal.impl.DefaultRepositoryLayoutProvider;
import org.eclipse.aether.internal.impl.Maven2RepositoryLayoutFactory;
import org.eclipse.aether.internal.test.util.TestUtils;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.repository.RepositoryPolicy;
import org.eclipse.aether.resolution.MetadataRequest;
import org.eclipse.aether.resolution.MetadataResult;
import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter;
import org.eclipse.aether.spi.connector.transport.PeekTask;
import org.eclipse.aether.spi.connector.transport.Transporter;
import org.eclipse.aether.spi.connector.transport.TransporterProvider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import static org.eclipse.aether.internal.impl.checksum.Checksums.checksumsSelector;
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.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * UT for {@link PrefixesRemoteRepositoryFilterSource} behavior when an <em>auto-discovered</em> prefixes file is
 * provably wrong: it denies a path that the remote repository actually serves. This is the "broken MRM leaks
 * member repository prefixes file over virtual repository" scenario from
 * <a href="https://github.com/apache/maven/issues/11856">apache/maven#11856</a>, observed in the wild on
 * {@code repo.jenkins-ci.org/public} (Artifactory virtual repository serving a stale 11-prefix file of a long-gone
 * member repository, breaking every Jenkins-ecosystem Maven 4 build).
 * <p>
 * Expected behavior: a denied path backed by an auto-discovered (i.e. not user-authored) prefixes file is verified
 * once against the remote repository; if the path exists remotely, the auto-discovered file is provably broken and
 * must be dropped for the rest of the session instead of taking the whole repository down.
 */
public class PrefixesRemoteRepositoryFilterSourceVerifyDeniedTest {
    /**
     * Faithful copy of the leaked file served by {@code https://repo.jenkins-ci.org/public/.meta/prefixes.txt}
     * (Last-Modified: 12 Jan 2017): valid magic, valid syntax, but the content belongs to a long-gone member
     * repository — {@code /org/jenkins-ci} is not listed.
     */
    private static final String LEAKED_PREFIXES = "## repository-prefixes/2.0\n"
            + "#\n"
            + "# Prefix file generated by Sonatype Nexus\n"
            + "# Do not edit, changes will be overwritten!\n"
            + "/de/regnis\n"
            + "/com/syntevo\n"
            + "/com/trilead\n"
            + "/org/eclipse\n"
            + "/org/tigris\n"
            + "/org/tmatesoft\n"
            + "/org/netbeans\n"
            + "/org/apache\n"
            + "/archetype-catalog.xml\n"
            + "/net/java\n"
            + "/archetype-catalog.xml.md5\n"
            + "/archetype-catalog.xml.sha1\n";

    private final Artifact jenkinsArtifact = new DefaultArtifact("org.jenkins-ci:version-number:1.14");

    @TempDir
    private Path tempDir;

    private DefaultRepositorySystemSession session;

    private RemoteRepository remoteRepository;

    private PrefixesRemoteRepositoryFilterSource subject;

    private Transporter transporter;

    @BeforeEach
    void setup() throws Exception {
        remoteRepository =
                new RemoteRepository.Builder("jenkins-public", "default", "https://irrelevant.example/").build();
        session = TestUtils.newSession();

        Path leakedPrefixesFile = tempDir.resolve("prefixes-jenkins-public.txt");
        Files.write(leakedPrefixesFile, LEAKED_PREFIXES.getBytes(StandardCharsets.UTF_8));

        // simulate successful auto-discovery: the remote repository serves the leaked file
        MetadataResolver metadataResolver = mock(MetadataResolver.class);
        when(metadataResolver.resolveMetadata(any(RepositorySystemSession.class), any(Collection.class)))
                .thenAnswer(invocation -> {
                    Collection<MetadataRequest> requests = invocation.getArgument(1);
                    MetadataRequest request = requests.iterator().next();
                    MetadataResult result = new MetadataResult(request);
                    result.setMetadata(request.getMetadata().setPath(leakedPrefixesFile));
                    return Collections.singletonList(result);
                });
        RemoteRepositoryManager remoteRepositoryManager = new RemoteRepositoryManager() {
            @Override
            public List<RemoteRepository> aggregateRepositories(
                    RepositorySystemSession session,
                    List<RemoteRepository> dominantRepositories,
                    List<RemoteRepository> recessiveRepositories,
                    boolean recessiveIsRaw) {
                return recessiveRepositories;
            }

            @Override
            public RepositoryPolicy getPolicy(
                    RepositorySystemSession session, RemoteRepository repository, boolean releases, boolean snapshots) {
                throw new UnsupportedOperationException("not implemented");
            }
        };
        DefaultRepositoryLayoutProvider layoutProvider = new DefaultRepositoryLayoutProvider(Collections.singletonMap(
                Maven2RepositoryLayoutFactory.NAME,
                new Maven2RepositoryLayoutFactory(
                        checksumsSelector(), new DefaultArtifactPredicateFactory(checksumsSelector()))));
        // existence checks against the remote repository: peek succeeds unless a test says otherwise
        transporter = mock(Transporter.class);
        TransporterProvider transporterProvider = mock(TransporterProvider.class);
        when(transporterProvider.newTransporter(any(), any())).thenReturn(transporter);
        subject = new PrefixesRemoteRepositoryFilterSource(
                new DefaultRepositoryKeyFunctionFactory(),
                () -> metadataResolver,
                () -> remoteRepositoryManager,
                layoutProvider,
                transporterProvider);
    }

    @Test
    void autoDiscoveredPrefixesDenyingExistingPathAreDropped() {
        RemoteRepositoryFilter filter = subject.getRemoteRepositoryFilter(session);
        assertNotNull(filter);

        // the artifact exists on the remote repository, despite what the leaked prefixes file claims;
        // the provably broken auto-discovered file must not take the repository down
        RemoteRepositoryFilter.Result result = filter.acceptArtifact(remoteRepository, jenkinsArtifact);

        assertTrue(
                result.isAccepted(),
                "provably broken auto-discovered prefixes must be dropped, got: " + result.reasoning());
    }

    @Test
    void brokenPrefixesAreDroppedForWholeRepository() throws Exception {
        RemoteRepositoryFilter filter = subject.getRemoteRepositoryFilter(session);
        assertNotNull(filter);

        assertTrue(filter.acceptArtifact(remoteRepository, jenkinsArtifact).isAccepted());
        // once dropped, other denied paths are accepted as well, without further existence checks
        assertTrue(filter.acceptArtifact(remoteRepository, new DefaultArtifact("io.jenkins:other:1.0"))
                .isAccepted());
        verify(transporter, times(1)).peek(any(PeekTask.class));
    }

    @Test
    void prefixesListedPathStaysAccepted() throws Exception {
        RemoteRepositoryFilter filter = subject.getRemoteRepositoryFilter(session);
        assertNotNull(filter);

        RemoteRepositoryFilter.Result result =
                filter.acceptArtifact(remoteRepository, new DefaultArtifact("org.eclipse.foo:bar:1.0"));

        assertTrue(result.isAccepted());
        verify(transporter, never()).peek(any(PeekTask.class));
    }

    @Test
    void deniedPathAbsentFromRemoteStaysDenied() throws Exception {
        doThrow(new Exception("404")).when(transporter).peek(any(PeekTask.class));
        RemoteRepositoryFilter filter = subject.getRemoteRepositoryFilter(session);
        assertNotNull(filter);

        // the artifact is neither covered by the prefixes file nor present on the remote repository:
        // the file is consistent with reality, the filter must keep denying
        assertFalse(filter.acceptArtifact(remoteRepository, jenkinsArtifact).isAccepted());
        // and verification cost is bounded: only the first denial is checked
        assertFalse(filter.acceptArtifact(remoteRepository, jenkinsArtifact).isAccepted());
        verify(transporter, times(1)).peek(any(PeekTask.class));
    }

    @Test
    void verificationCanBeDisabled() throws Exception {
        session.setConfigProperty("aether.remoteRepositoryFilter.prefixes.verifyDenied", "false");
        RemoteRepositoryFilter filter = subject.getRemoteRepositoryFilter(session);
        assertNotNull(filter);

        assertFalse(filter.acceptArtifact(remoteRepository, jenkinsArtifact).isAccepted());
        verify(transporter, never()).peek(any(PeekTask.class));
    }

    @Test
    void offlineSessionSkipsVerification() throws Exception {
        session.setOffline(true);
        RemoteRepositoryFilter filter = subject.getRemoteRepositoryFilter(session);
        assertNotNull(filter);

        assertFalse(filter.acceptArtifact(remoteRepository, jenkinsArtifact).isAccepted());
        verify(transporter, never()).peek(any(PeekTask.class));
    }

    @Test
    void brokenPrefixesDroppedButNoInputOutcomeFalseStillDenies() throws Exception {
        session.setConfigProperty("aether.remoteRepositoryFilter.prefixes.noInputOutcome", "false");
        RemoteRepositoryFilter filter = subject.getRemoteRepositoryFilter(session);
        assertNotNull(filter);

        // the broken file is detected and dropped (peek happened), but the user explicitly asked for
        // strict filtering when no trustworthy input is available: the path stays denied
        assertFalse(filter.acceptArtifact(remoteRepository, jenkinsArtifact).isAccepted());
        verify(transporter, times(1)).peek(any(PeekTask.class));
    }

    @Test
    void userProvidedPrefixesAreAuthoritative() throws Exception {
        Path baseDir = session.getLocalRepository()
                .getBasePath()
                .resolve(PrefixesRemoteRepositoryFilterSource.LOCAL_REPO_PREFIX_DIR);
        Files.createDirectories(baseDir);
        Files.write(baseDir.resolve("prefixes-jenkins-public.txt"), LEAKED_PREFIXES.getBytes(StandardCharsets.UTF_8));
        RemoteRepositoryFilter filter = subject.getRemoteRepositoryFilter(session);
        assertNotNull(filter);

        // deliberately user-authored prefixes are never second-guessed
        assertFalse(filter.acceptArtifact(remoteRepository, jenkinsArtifact).isAccepted());
        verify(transporter, never()).peek(any(PeekTask.class));
    }
}
