package com.atlassian.bitbucket.internal.mirroring.mirror.web;

import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.json.JsonRenderer;
import com.atlassian.bitbucket.mirroring.mirror.UpstreamServer;
import com.atlassian.bitbucket.mirroring.mirror.UpstreamService;
import com.atlassian.bitbucket.server.ApplicationMode;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.atlassian.bitbucket.util.Operation;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import javax.servlet.FilterChain;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.io.PrintWriter;

import static com.atlassian.bitbucket.util.DevModeUtils.ATLASSIAN_DEV_MODE;
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class MirrorInterceptorFilterTest {
    private static final String UPSTREAM_URL = "https://mirror.stash.atlassian.com";
    private static final String CONTEXT_PATH = "/bitbucket";

    @Mock
    private FilterChain chain;
    @Mock
    private RequestDispatcher dispatcher;
    private SimpleI18nService i18nService = new SimpleI18nService();
    @Mock
    private JsonRenderer jsonRenderer;
    @Mock
    private ApplicationPropertiesService propertiesService;
    @Mock
    private HttpServletRequest request;
    @Mock
    private HttpServletResponse response;
    @Mock
    private UpstreamService upstreamService;

    @Before
    public void setUp() throws Exception {
        when(propertiesService.isSetup()).thenReturn(true);
        when(jsonRenderer.render(anyObject(), any())).thenReturn("{}");
        when(request.getContentType()).thenReturn(MediaType.TEXT_HTML);
        PrintWriter writer = mock(PrintWriter.class);
        when(response.getWriter()).thenReturn(writer);
        UpstreamServer upstream = mock(UpstreamServer.class);
        when(upstream.getBaseUrl()).thenReturn(UPSTREAM_URL);
        when(upstreamService.get()).thenReturn(upstream);
    }

    @Test
    public void testNotRedirectedIfNotMirror() throws Exception {
        doFilter(ApplicationMode.DEFAULT, "/project");

        verify(chain).doFilter(request, response);
        verifyZeroInteractions(response);
    }

    @Test
    public void testMirrorsForwardedToServlet() throws Exception {
        doFilter(ApplicationMode.MIRROR, MirrorInterceptorFilter.PATH_WEB_MIRROR);

        verify(request).getRequestDispatcher(MirrorInterceptorFilter.PATH_PLACEHOLDER_PAGE);
        verify(dispatcher).forward(request, response);
        verifyZeroInteractions(response);
        verifyZeroInteractions(chain);
    }

    @Test
    public void testRedirectsInMirrorMode() throws Exception {
        doFilter(ApplicationMode.MIRROR, "/projects");

        verifyRedirected();
    }

    @Test
    public void testWhitelistAdminPage() throws Exception {
        doFilter(ApplicationMode.MIRROR, "/plugins/servlet/admin/mirror");

        verifyPassThrough();
    }

    @Test
    public void testWhitelistAboutPage() throws Exception {
        doFilter(ApplicationMode.MIRROR, "/about");

        verifyPassThrough();
    }

    @Test
    public void testWhitelistErrorPage() throws Exception {
        doFilter(ApplicationMode.MIRROR, "/error500");

        verifyPassThrough();
    }

    @Test
    public void testWhitelistStartupPage() throws Exception {
        doFilter(ApplicationMode.MIRROR, "/system/startup");

        verifyPassThrough();
    }

    @Test
    public void testWhitelistStaticResources() throws Exception {
        doFilter(ApplicationMode.MIRROR, "/s/f40f76602a28604ad5456b4ca4a6f7d6-CDN/en_US/$%7Bcommi/43/43/_/download/superbatch/js/batch.js?locale=en-US");

        verifyPassThrough();
    }

    @Test
    public void testWhitelistStatus() throws Exception {
        doFilter(ApplicationMode.MIRROR, "/status");

        verifyPassThrough();
    }

    @Test
    public void testWhitelistStpWeb() throws Exception {
        doFilter(ApplicationMode.MIRROR, "/plugins/servlet/stp/view");

        verify(chain).doFilter(request, response);
        //verify that the decorator has been overridden and the connect host URL has been provided
        verify(request).setAttribute("decorator.override", "bitbucket.internal.connect.page");
        verify(request).setAttribute("connectHostUrl", UPSTREAM_URL);
        verifyZeroInteractions(response);
        verifyZeroInteractions(dispatcher);
    }

    @Test
    public void testWhitelistStpWebNoUpstream() throws Exception {
        when(upstreamService.get()).thenReturn(null);
        doFilter(ApplicationMode.MIRROR, "/plugins/servlet/stp/view");

        verifyRedirected();
    }

    @Test
    public void testWhitelistStpRest() throws Exception {
        doFilter(ApplicationMode.MIRROR, "/rest/stp/latest/service-request/99");

        verifyPassThrough();
    }


    @Test
    public void testWhitelistRestApplicationPropertiesEndpoint() throws Exception {
        doFilter(ApplicationMode.MIRROR, "/rest/api/latest/application-properties");

        verifyPassThrough();
    }

    @Test
    public void testWhitelistMirroringLfsRestStorageEndPoint() throws Exception {
        doFilter(ApplicationMode.MIRROR, "/rest/git-lfs/storage/A/B/eed08a1edb5eeea08ea4eb14619ac8494342ba3fd24bd4b423b4c3cf1427fb9b");

        verifyPassThrough();
    }

    @Test
    public void testWhitelistMirroringRestEndpoint() throws Exception {
        doFilter(ApplicationMode.MIRROR, "/rest/mirroring/latest/foo/bar");

        verifyPassThrough();
    }

    @Test
    public void testDontWhitelistAnyOtherRestEndpoint() throws Exception {
        doFilter(ApplicationMode.MIRROR, "/rest/foo/latest/foo/bar");

        verifyJsonForbidden();
    }

    @Test
    public void testWhitelistAnyOtherRestEndpointInDevMode() throws Exception {
        inDevMode(() -> {
            doFilter(ApplicationMode.MIRROR, "/rest/foo/latest/foo/bar");
            verifyPassThrough();
            return null;
        });
    }

    @Test
    public void testWhitelistScmRequests() throws Exception {
        doFilter(ApplicationMode.MIRROR, "/scm/");

        verifyPassThrough();
    }

    @Test
    public void testWhitelistGitRequests() throws Exception {
        doFilter(ApplicationMode.MIRROR, "/git/");

        verifyPassThrough();
    }

    @Test
    public void testFilterBypassedWhenInSetupMode() throws Exception {
        inSetupMode();

        doFilter(ApplicationMode.MIRROR, "/projects");

        verifyPassThrough();
    }

    private void inSetupMode() {
        when(propertiesService.isSetup()).thenReturn(false);
    }

    private void doFilter(ApplicationMode mode, String url) throws IOException, ServletException {
        when(request.getRequestDispatcher(anyString())).thenReturn(dispatcher);
        when(request.getRequestURI()).thenReturn(CONTEXT_PATH + url);
        when(request.getContextPath()).thenReturn(CONTEXT_PATH);
        when(propertiesService.getMode()).thenReturn(mode);
        MirrorInterceptorFilter filter = new MirrorInterceptorFilter(i18nService, jsonRenderer, propertiesService,
                upstreamService);

        filter.doFilter(request, response, chain);
    }

    private <T, E extends Exception> T inDevMode(Operation<T, E> operation) throws E {
        String oldDevMode = System.getProperty(ATLASSIAN_DEV_MODE);
        System.setProperty(ATLASSIAN_DEV_MODE, "true");
        try {
            return operation.perform();
        } finally {
            if (oldDevMode == null) {
                System.clearProperty(ATLASSIAN_DEV_MODE);
            } else {
                System.setProperty(ATLASSIAN_DEV_MODE, oldDevMode);
            }
        }
    }

    private void verifyJsonForbidden() {
        verify(request, never()).setAttribute(any(), any());
        verify(response).setContentType(APPLICATION_JSON);
        verify(response).setStatus(SC_FORBIDDEN);
    }

    private void verifyPassThrough() throws IOException, ServletException {
        verify(chain).doFilter(request, response);
        verify(request, never()).setAttribute(any(), any());
        verifyZeroInteractions(response);
        verifyZeroInteractions(dispatcher);
    }

    private void verifyRedirected() throws IOException {
        verify(response).sendRedirect(CONTEXT_PATH + MirrorInterceptorFilter.PATH_WEB_MIRROR);
        verify(request, never()).setAttribute(any(), any());
        verifyZeroInteractions(dispatcher);
        verifyZeroInteractions(chain);
    }
}