package com.atlassian.stash.internal.scm.git.protocol.http;

import com.atlassian.bitbucket.AuthorisationException;
import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.help.HelpPathService;
import com.atlassian.bitbucket.hook.HookService;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.mockito.MockitoUtils;
import com.atlassian.bitbucket.permission.PermissionService;
import com.atlassian.bitbucket.project.Project;
import com.atlassian.bitbucket.repository.NoSuchRepositoryException;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.RepositoryService;
import com.atlassian.bitbucket.request.RequestContext;
import com.atlassian.bitbucket.request.RequestManager;
import com.atlassian.bitbucket.scm.AuthenticationState;
import com.atlassian.bitbucket.scm.git.command.GitCommandBuilderFactory;
import com.atlassian.bitbucket.scm.http.HttpRequestDetails;
import com.atlassian.bitbucket.scm.http.HttpScmRequest;
import com.atlassian.bitbucket.scm.throttle.ThrottledScmRequestFactory;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.plugin.servlet.ServletContextFactory;
import com.atlassian.stash.internal.scm.git.InternalGitScmConfig;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.mock.web.DelegatingServletOutputStream;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.util.Optional;

import static com.atlassian.stash.internal.scm.git.protocol.http.HttpScmRequestMatchers.*;
import static java.util.Optional.empty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class DefaultGitHttpScmRequestHandlerTest {

    @Mock
    private AuthenticationContext authenticationContext;
    @Mock
    private GitCommandBuilderFactory builderFactory;
    @Mock
    private InternalGitScmConfig config;
    @Mock
    private EventPublisher eventPublisher;
    @Mock
    private HelpPathService helpPathService;
    @Mock
    private HookService hookService;
    @Spy
    private I18nService i18nService = new SimpleI18nService();
    @Mock
    private PermissionService permissionService;
    @Mock
    private RepositoryService repositoryService;
    @InjectMocks
    private DefaultGitHttpScmRequestHandler requestHandler;
    @Mock
    private RequestManager requestManager;
    @Mock
    private HttpServletResponse response;
    @Mock
    private ServletContextFactory servletContextFactory;
    @Mock
    private ThrottledScmRequestFactory throttledFactory;

    @Before
    public void setup() {
        when(throttledFactory.throttled(any(HttpScmRequest.class))).thenAnswer(MockitoUtils.returnFirst());
        when(repositoryService.getBySlug(null, "project", "slug")).thenReturn(mock(Repository.class));
        when(authenticationContext.isAuthenticated()).thenReturn(true);
    }

    //===================================================================================================
    // Tests that verify that 'smart' write requests are returned
    //===================================================================================================

    /** Tests that git-receive-pack (as per the 'service' query argument) is a 'smart' write operation */
    @Test
    public void receivePackServiceIsWrite() {
        mockRepository(null, "project", "slug");
        HttpServletRequest request = mockRequest("GET", "/project/slug.git", "git-receive-pack");

        HttpScmRequest scmRequest = assertSupported(request);

        assertThat(scmRequest, is(smartWrite()));
        // only /project/slug.git/git-<receive|upload>-pack is throttled since the other requests (e.g info/refs)
        // are not as resource intensive
        verify(throttledFactory, never()).throttled(any(HttpScmRequest.class));
    }

    /** Tests that git-receive-pack (as per the path info) is a 'smart' write operation */
    @Test
    public void receivePackPathInfoIsWrite() {
        mockRepository(null, "project", "slug");
        HttpServletRequest request = mockRequest("POST", "/project/slug.git/git-receive-pack");

        HttpScmRequest scmRequest = assertSupported(request);

        assertThat(scmRequest, is(smartWrite()));
        verify(throttledFactory).throttled(any(HttpScmRequest.class));
    }

    //===================================================================================================
    // Tests that verify that 'smart' read (non-write) requests are returned
    //===================================================================================================

    /** Tests that git-upload-pack (as per the 'service' query argument) is a 'smart' read operation */
    @Test
    public void uploadPackServiceIsRead() {
        mockRepository(null, "project", "slug");
        HttpServletRequest request = mockRequest("POST", "/project/slug.git", "git-upload-pack");

        HttpScmRequest scmRequest = assertSupported(request);

        assertThat(scmRequest, is(smartRead()));
        // only /project/slug.git/git-<receive|upload>-pack is throttled since the other requests (e.g info/refs)
        // are not as resource intensive
        verify(throttledFactory, never()).throttled(any(HttpScmRequest.class));
    }

    /** Tests that git-upload-pack (as per the path info) is a 'smart' read operation */
    @Test
    public void uploadPackPathInfoIsNotWrite() {
        mockRepository(null, "project", "slug");
        HttpServletRequest request = mockRequest("POST", "/project/slug.git/git-upload-pack");
        when(requestManager.getRequestContext()).thenReturn(mock(RequestContext.class));

        HttpScmRequest scmRequest = assertSupported(request);

        assertThat(scmRequest, is(smartRead()));
        verify(throttledFactory).throttled(any(HttpScmRequest.class));
    }

    //===================================================================================================
    // Tests for the finding of repositories based on the contents of URL's.
    //===================================================================================================

    /**
     * Tests that a request handler finds a repository by its slug when a valid project key and
     * slug is provided to it in the path info.
     */
    @Test
    public void findsRepoBySlug() {
        HttpServletRequest request = mockRequest("GET", "/project/slug.git", "git-upload-pack");
        Repository repo = mockRepository(null, "project", "slug");

        // Does the returned scm request have the mock repository?
        HttpScmRequest scmRequest = assertSupported(request);

        assertThat(scmRequest, hasRepo(repo));
        // only /project/slug.git/git-<receive|upload>-pack is throttled since the other requests (e.g info/refs)
        // are not as resource intensive
        verify(throttledFactory, never()).throttled(any(HttpScmRequest.class));
    }

    @Test
    public void findsRepoBySlugWhenNamespaced() {
        HttpServletRequest request = mockRequest("GET", "/namespace/project/slug.git", "git-upload-pack");
        Repository repo = mockRepository("namespace", "project", "slug");

        // Does the returned scm request have the mock repository?
        HttpScmRequest scmRequest = assertSupported(request);

        assertThat(scmRequest, hasRepo(repo));
        // only /project/slug.git/git-<receive|upload>-pack is throttled since the other requests (e.g info/refs)
        // are not as resource intensive
        verify(throttledFactory, never()).throttled(any(HttpScmRequest.class));
    }

    @Test(expected = NoSuchRepositoryException.class)
    public void noSuchRepositoryWhenNoPathInfo() {
        HttpServletRequest request = mockRequest("GET", null, "git-upload-pack");

        requestHandler.create(request, response);
    }

    @Test(expected = NoSuchRepositoryException.class)
    public void nullRepositoryWhenInvalidPathInfo() {
        HttpServletRequest request = mockRequest("GET", "baloney", "git-upload-pack");

        requestHandler.create(request, response);
    }

    @Test(expected = AuthorisationException.class)
    public void unauthenticatedWrite() {
        when(authenticationContext.isAuthenticated()).thenReturn(false);
        HttpServletRequest request = mockRequest("GET", "/project/slug.git", "git-receive-pack");

        requestHandler.create(request, response);
    }

    @Test
    public void namespacedRepoReceivePack() {
        mockRepository("namespace", "project", "slug");
        HttpServletRequest request = mockRequest("POST", "/namespace/project/slug.git/git-receive-pack");

        HttpScmRequest scmRequest = assertSupported(request);

        assertThat(scmRequest, is(smartWrite()));
        verify(throttledFactory).throttled(any(HttpScmRequest.class));
    }

    @Test
    public void namespacedRepoInfoRefsReceivePack() {
        mockRepository("namespace", "project", "slug");
        HttpServletRequest request = mockRequest("GET", "/namespace/project/slug.git/info/refs", "git-receive-pack");

        assertSupported(request);

        verify(throttledFactory, never()).throttled(any(HttpScmRequest.class));
    }

    @Test
    public void namespacedRepoUploadPack() {
        mockRepository("namespace", "project", "slug");
        HttpServletRequest request = mockRequest("POST", "/namespace/project/slug.git/git-upload-pack");

        HttpScmRequest scmRequest = assertSupported(request);

        assertThat(scmRequest, is(smartRead()));
        verify(throttledFactory).throttled(any(HttpScmRequest.class));
    }

    @Test(expected = NoSuchRepositoryException.class)
    public void namespacedRepoInvalidPathInfo() {
        HttpServletRequest request = mockRequest("POST", "/namespace/project/slug.git/baloney", "git-upload-pack");
        assertSupported(request);
    }

    //===================================================================================================
    // Tests for 'dumb' git protocol
    //
    // The following tests throw various kinds of requests at the request handler, all of them lacking a
    // 'service' query parameter. In every case, the request handler should return an instance of
    // GitInvalidHttpScmRequest.
    //
    // The path info scenarios were pinched from struct service_cmd in the git source code file http-backend.c
    //===================================================================================================

    @Test
    public void headWithoutServiceParamInvalid() {
        HttpServletRequest request = mockRequest("GET", "/project/slug.git/HEAD");

        assertUnsupported(request);
    }

    @Test
    public void infoRefsWithoutServiceParamInvalid() {
        HttpServletRequest request = mockRequest("GET", "/project/slug.git/info/refs");

        assertUnsupported(request);
    }

    @Test
    public void alternatesWithoutServiceParamInvalid() {
        HttpServletRequest request = mockRequest("GET", "/project/slug.git/objects/info/alternates");

        assertUnsupported(request);
    }

    @Test
    public void httpAlternatesWithoutServiceParamInvalid() {
        HttpServletRequest request = mockRequest("GET", "/project/slug.git/objects/info/http-alternates");

        assertUnsupported(request);
    }

    @Test
    public void infoPacksWithoutServiceParamInvalid() {
        HttpServletRequest request = mockRequest("GET", "/project/slug.git/objects/info/packs");

        assertUnsupported(request);
    }

    @Test
    public void looseObjectWithoutServiceParamInvalid() {
        HttpServletRequest request = mockRequest("GET", "/project/slug.git/objects/ca/82a6dff817ec66f44342007202690a93763949");

        assertUnsupported(request);
    }

    @Test
    public void getPackFileWithoutServiceParamInvalid() {
        HttpServletRequest request = mockRequest("GET", "/project/slug.git/objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack");

        assertUnsupported(request);
    }

    @Test
    public void getIdxFileWithoutServiceParamInvalid() {
        HttpServletRequest request = mockRequest("GET", "/project/slug.git/objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.idx");

        assertUnsupported(request);
    }

    @Test
    public void namespacedRepogetIdxFileWithoutServiceParamInvalid() {
        HttpServletRequest request = mockRequest("GET", "/namespace/project/slug.git/objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.idx");

        assertUnsupported(request);
    }

    /**
     * Tests that a 'bare' git URL (one that ends with .git, and has no path info) is not handled.
     */
    @Test
    public void bareGitUrlNotHandled() {
        HttpServletRequest request = mockRequest("GET", "/project/slug.git");

        assertUnsupported(request);
    }

    @Test
    public void testNotAuthenticated() throws Exception {
        HttpServletRequest request = mockRequest("GET", "/project/slug.git");

        requestHandler.sendAuthenticationError(AuthenticationState.NOT_AUTHENTICATED, "test", "details", request, response);

        verify(response).setHeader(eq("WWW-Authenticate"), anyString());
        verify(response).setStatus(eq(HttpServletResponse.SC_UNAUTHORIZED));
    }

    @Test
    public void testCaptchaRequired() throws Exception {
        HttpServletRequest request = mockRequest("GET", "/project/slug.git");
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        when(response.getOutputStream()).thenReturn(new DelegatingServletOutputStream(stream));

        for (AuthenticationState state : AuthenticationState.values()) {
            if (state != AuthenticationState.NOT_AUTHENTICATED) {
                requestHandler.sendAuthenticationError(state, "test", "details", request, response);

                String content = stream.toString();
                assertTrue(content.contains("test") && content.contains("details"));

                verify(response, atLeastOnce()).setStatus(eq(HttpServletResponse.SC_OK));

                stream.reset();
            }
        }
    }

    //===================================================================================================
    // Utility methods
    //===================================================================================================

    private HttpServletRequest mockRequest(String method, String pathInfo) {
        return mockRequest(method, pathInfo, null);
    }

    /**
     * Constructs a mock HTTP servlet request with the given method, optional path info, and optional service.
     *
     * @return a new mock HTTP servlet request
     */
    private HttpServletRequest mockRequest(String method, String pathInfo, String service) {
        HttpServletRequest request = mock(HttpServletRequest.class);
        when(request.getMethod()).thenReturn(method);
        when(request.getPathInfo()).thenReturn(pathInfo);
        when(request.getParameter("service")).thenReturn(service);
        return request;
    }

    private HttpScmRequest assertSupported(HttpServletRequest request) {
        assertThat(requestHandler.supports(HttpRequestDetails.ofServletRequest(request)), is(true));
        Optional<HttpScmRequest> result = requestHandler.create(request, response);
        assertThat(result, not(empty()));
        //noinspection OptionalGetWithoutIsPresent
        return result.get();
    }

    private void assertUnsupported(HttpServletRequest request) {
        assertThat(requestHandler.supports(HttpRequestDetails.ofServletRequest(request)), is(false));
        assertThat(requestHandler.create(request, response), is(empty()));

        verifyZeroInteractions(throttledFactory);
    }

    private Repository mockRepository(String projectNamespace, String projectKey, String repoSlug) {
        Project project = mock(Project.class);
        Repository repository = mock(Repository.class);

        when(project.getNamespace()).thenReturn(projectNamespace);
        when(project.getKey()).thenReturn(projectKey);
        when(repository.getSlug()).thenReturn(repoSlug);
        when(repository.getProject()).thenReturn(project);

        when(repositoryService.getBySlug(projectNamespace, projectKey, repoSlug)).thenReturn(repository);

        return repository;
    }
}

