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

import com.atlassian.bitbucket.internal.mirroring.mirror.InternalUpstreamServer;
import com.atlassian.bitbucket.internal.mirroring.mirror.InternalUpstreamService;
import com.atlassian.bitbucket.internal.mirroring.mirror.nav.MirroringNavBuilder;
import com.atlassian.bitbucket.internal.mirroring.mirror.nav.MirroringUrl;
import com.atlassian.bitbucket.internal.mirroring.mirror.nav.StubMirroringNavBuilder;
import com.atlassian.bitbucket.mirroring.mirror.IntegrationState;
import com.atlassian.bitbucket.mirroring.mirror.UpstreamServerType;
import com.atlassian.bitbucket.server.ApplicationMode;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.atlassian.soy.renderer.SoyTemplateRenderer;
import com.google.common.collect.ImmutableMap;
import com.google.common.net.MediaType;
import org.apache.commons.lang.StringUtils;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

import static com.atlassian.bitbucket.internal.mirroring.mirror.web.MirrorWebConstants.PLUGIN_KEY;
import static com.atlassian.bitbucket.internal.mirroring.mirror.web.MirrorWebConstants.TEMPLATE_MODULE_KEY;
import static java.util.Optional.ofNullable;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;

public class MirrorServletTest {

    private static final String PLACEHOLDER_MODULE_KEY = PLUGIN_KEY + ":mirror-placeholder-page-resources";
    private static final String SERVER_ID = "BZKJ-HSFI-H4HM-QKX7";
    private static final String TEMPLATE_CONNECTING = "bitbucket.internal.mirror.page.connecting";
    private static final String TEMPLATE_PENDING = "bitbucket.internal.mirror.page.pending";
    private static final String TEMPLATE_ACTIVE = "bitbucket.internal.mirror.page.active";
    private static final String TEMPLATE_REMOVED = "bitbucket.internal.mirror.page.removed";
    private static final String TEMPLATE_UNKNOWN = "bitbucket.internal.mirror.page.unknown";

    @Rule
    public MockitoRule rule = MockitoJUnit.rule();

    @Mock
    private InternalUpstreamService service;
    @Mock
    private SoyTemplateRenderer templateRenderer;
    @Spy
    private MirroringNavBuilder navBuilder = new StubMirroringNavBuilder();
    @Mock
    private HttpServletRequest servletRequest;
    @Mock
    private HttpServletResponse servletResponse;
    @Mock
    private InternalUpstreamServer upstream;
    @Mock
    private ApplicationPropertiesService propertiesService;
    @InjectMocks
    private MirrorServlet servlet;

    @Before
    public void before() {
        when(propertiesService.getMode()).thenReturn(ApplicationMode.MIRROR);
        when(propertiesService.getServerId()).thenReturn(SERVER_ID);
    }

    @Test
    public void testGetWithNoUpstream() {
        assertNoUpstream();
    }

    @Test
    public void testGetWithNoUpstreamIgnoresErrors() {
        withRequestError("I'm a request error", "nothing");
        assertNoUpstream();
    }

    @Test
    public void testGetWithUpstreamInInitializing() {
        assertInitializingUpstream();
    }

    @Test
    public void testGetWithUpstreamInInitializingIgnoresErrors() {
        withRequestError("I am an error", "nothing");
        assertInitializingUpstream();
    }

    @Test
    public void testGetWithUpstreamInPending() {
        assertView(IntegrationState.PENDING, TEMPLATE_PENDING);
    }

    @Test
    public void testGetWithUpstreamInPendingShowsError() {
        String errorMessage = "I am an error";

        withRequestError(errorMessage, "nothing");
        assertView(IntegrationState.PENDING, TEMPLATE_PENDING, errorMessage);
    }

    @Test
    public void testGetWithUpstreamInPendingIgnoresAccessError() {
        withRequestError("errorMessage", "access_denied");
        assertView(IntegrationState.PENDING, TEMPLATE_PENDING);
    }

    @Test
    public void testGetWithCloudUpstreamInPendingAndSetupParameterWillRedirectToAuthorizePage() {
        String url = "http://example.com";

        withUpstream(url)
                .inState(IntegrationState.PENDING)
                .ofType(UpstreamServerType.BITBUCKET_CLOUD);

        withParameterSetup(true);

        doGet();

        assertRedirect(navBuilder.upstream(upstream).authorizeAddon());
        assertNoRender();
    }

    @Test
    public void testGetWithCloudUpstreamInPendingAndNoSetupParameterDoesntRedirectToAuthorizePage() {
        String url = "http://example.com";

        withUpstream(url)
                .inState(IntegrationState.PENDING)
                .ofType(UpstreamServerType.BITBUCKET_CLOUD);

        withParameterSetup(false);

        doGet();

        assertRendered(IntegrationState.PENDING, TEMPLATE_PENDING, url);
        assertNoRedirect();
    }

    @Test
    public void testGetWithServerUpstreamInPendingAndSetupParameterWillRedirectToSelf() {
        String url = "http://example.com";

        withUpstream(url)
                .inState(IntegrationState.PENDING)
                .ofType(UpstreamServerType.BITBUCKET_SERVER);

        withParameterSetup(true);

        doGet();

        assertRedirectToPlaceHolder();
        assertNoRender();
    }

    @Test
    public void testGetWithUpstreamInInstalled() {
        assertView(IntegrationState.INSTALLED, TEMPLATE_ACTIVE);
    }

    @Test
    public void testGetWithUpstreamInInstalledAndSetupParameterReloadsPage() {
        assertRedirectToSelfWithSetupParameterAndState(IntegrationState.INSTALLED);
    }

    @Test
    public void testGetWithUpstreamInInstalledRedirectsForCloudAdminAfterInstall() {

        String clientKey = "clientKey";

        withUpstream("http://example.com")
                .inState(IntegrationState.INSTALLED)
                .ofType(UpstreamServerType.BITBUCKET_CLOUD)
                .withParameterIssuerId(clientKey);

        withClientKey(clientKey);

        String redirectUrl = navBuilder.upstream(upstream).admin().getAbsolute();

        doGet();

        assertRedirect(redirectUrl);
        assertNoRender();
    }

    @Test
    public void testGetWithUpstreamInInstalledDoesntRedirectToCloudIfClientKeyDoesntMatch() {

        String clientKey = "clientKey";
        String url = "http://example.com";

        withUpstream(url)
                .inState(IntegrationState.INSTALLED)
                .ofType(UpstreamServerType.BITBUCKET_CLOUD)
                .withParameterIssuerId(clientKey);

        withClientKey(clientKey + "NoMatch");

        doGet();

        assertRendered(IntegrationState.INSTALLED, TEMPLATE_ACTIVE, url);
        assertNoRedirect();
    }

    @Test
    public void testGetWithUpstreamInInstalledDoesntRedirectToCloudIfServerUpstream() {

        String clientKey = "clientKey";
        String url = "http://example.com";

        withUpstream(url)
                .inState(IntegrationState.INSTALLED)
                .ofType(UpstreamServerType.BITBUCKET_SERVER)
                .withParameterIssuerId(clientKey);

        withClientKey(clientKey);

        doGet();

        assertRendered(IntegrationState.INSTALLED, TEMPLATE_ACTIVE, url);
        assertNoRedirect();
    }

    @Test
    public void testGetWithUpstreamInstalledIgnoresErrors() {
        withRequestError("I am an error", "nothing");
        assertView(IntegrationState.INSTALLED, TEMPLATE_ACTIVE);
    }

    @Test
    public void testGetWithUpstreamInRemoved() {
        assertRemovedView();
    }

    @Test
    public void testGetWithUpstreamInRemovedAndSetupParameterReloadsPage() {
        assertRedirectToSelfWithSetupParameterAndState(IntegrationState.REMOVED);
    }

    @Test
    public void testGetWithUpstreamInRemovedIgnoresErrors() {
        withRequestError("I am an error", "nothing");
        assertRemovedView();
    }

    @Test
    public void testGetWithUpstreamInUnknown() {
        assertView(IntegrationState.UNKNOWN, TEMPLATE_UNKNOWN);
    }

    @Test
    public void testGetWithUpstreamInUnknownAndSetupParameterReloadsPage() {
        assertRedirectToSelfWithSetupParameterAndState(IntegrationState.UNKNOWN);
    }

    @Test
    public void testGetWithUpstreamInUnknownIgnoresErrors() {
        withRequestError("I'm an error that is going to be ignored.", "nothing");
        assertView(IntegrationState.UNKNOWN, TEMPLATE_UNKNOWN);
    }

    @Test
    public void testGetPlaceholderOnDefault() throws Exception {
        when(propertiesService.getMode()).thenReturn(ApplicationMode.DEFAULT);

        servlet.doGet(servletRequest, servletResponse);

        verify(servletResponse).sendError(HttpServletResponse.SC_NOT_FOUND);
    }

    private static void assertHtmlContentType(HttpServletResponse servletResponse) {
        verify(servletResponse).setContentType(MediaType.HTML_UTF_8.toString());
    }

    private void assertInitializingUpstream() {

        String url = "https://example.com";
        withUpstream(url).inState(IntegrationState.INITIALIZING);

        doGet();

        Map<String, Object> expectedTemplateArgs = ImmutableMap.<String, Object>builder()
                .put("mirrorState", IntegrationState.INITIALIZING.getId())
                .put("upstreamUrl", url)
                .put("upstream", "example.com")
                .build();

        assertHtmlContentType(servletResponse);
        verify(templateRenderer).render(any(), eq(TEMPLATE_MODULE_KEY), eq(TEMPLATE_CONNECTING), eq(expectedTemplateArgs));
    }

    private void assertNoRedirect() {
        try {
            verify(servletResponse, never()).sendRedirect(anyString());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private void assertNoRender() {
        verifyZeroInteractions(templateRenderer);
    }

    private void assertNoUpstream() {
        doGet();

        Map<String, Object> expectedTemplateArgs = ImmutableMap.<String, Object>builder()
                .put("mirrorState", IntegrationState.INITIALIZING.getId())
                .put("upstreamUrl", "")
                .put("upstream", "")
                .build();

        assertHtmlContentType(servletResponse);
        verify(templateRenderer).render(any(), eq(TEMPLATE_MODULE_KEY), eq(TEMPLATE_CONNECTING), eq(expectedTemplateArgs));
    }

    private void assertRedirectToSelfWithSetupParameterAndState(IntegrationState state) {
        withUpstream("http://example.com")
                .ofType(UpstreamServerType.BITBUCKET_CLOUD)
                .inState(state);

        withParameterSetup(true);

        doGet();

        assertNoRender();
        assertRedirectToPlaceHolder();
    }

    private void assertRendered(IntegrationState state, String template, String url) {
        assertRendered(state, template, url, null);
    }

    private void assertRendered(IntegrationState state, String template, String url, String error) {
        Map<String, Object> expectedTemplateArgs = ImmutableMap.<String, Object>builder()
                .put("mirrorState", state.getId())
                .put("upstreamUrl", url)
                .put("upstream", "example.com")
                .put("registrationUrl", navBuilder.upstream(upstream).authorizeAddon().getAbsolute())
                .put("serverId", SERVER_ID)
                .put("moduleKey", PLACEHOLDER_MODULE_KEY)
                .put("type", upstream.getType().getId())
                .put("error", StringUtils.stripToEmpty(error))
                .build();

        assertHtmlContentType(servletResponse);
        verify(templateRenderer).render(any(), eq(TEMPLATE_MODULE_KEY), eq(template), eq(expectedTemplateArgs));
    }

    private void assertRedirect(MirroringUrl url) {
        assertRedirect(url.getAbsolute());
    }

    private void assertRedirect(String url) {
        try {
            verify(servletResponse).sendRedirect(url);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private void assertRedirectToPlaceHolder() {
        assertRedirect(navBuilder.placeholder());
    }

    private void assertRemovedView() {
        String url = "https://example.com";
        withUpstream(url).inState(IntegrationState.REMOVED);

        doGet();

        Map<String, Object> expectedTemplateArgs = ImmutableMap.<String, Object>builder()
                .put("mirrorState", IntegrationState.REMOVED.getId())
                .put("upstreamUrl", url)
                .put("upstream", "example.com")
                .put("reconnectUrl", navBuilder.placeholder().getAbsolute() + "?reconnect")
                .put("moduleKey", PLACEHOLDER_MODULE_KEY)
                .build();

        assertHtmlContentType(servletResponse);
        verify(templateRenderer).render(any(), eq(TEMPLATE_MODULE_KEY), eq(TEMPLATE_REMOVED), eq(expectedTemplateArgs));
    }

    private void assertView(IntegrationState state, String template) {
        assertView(state, template, null);
    }

    private void assertView(IntegrationState state, String template, String error) {
        String url = "https://example.com";
        withUpstream(url).inState(state).ofType(UpstreamServerType.BITBUCKET_CLOUD);

        doGet();

        assertRendered(state, template, url, error);
    }

    private void doGet() {
        try {
            servlet.doGet(servletRequest, servletResponse);
        } catch (ServletException | IOException e) {
            throw new RuntimeException(e);
        }
    }

    private MirrorServletTest inState(IntegrationState state) {
        when(upstream.getState()).thenReturn(state);
        return this;
    }

    private MirrorServletTest ofType(UpstreamServerType type) {
        when(upstream.getType()).thenReturn(type);
        return this;
    }

    private MirrorServletTest withClientKey(String key) {
        when(servletRequest.getParameter("clientKey")).thenReturn(key);
        return this;
    }

    private MirrorServletTest withParameterIssuerId(String issuerId) {
        when(upstream.getIssuerId()).thenReturn(ofNullable(issuerId));
        return this;
    }

    private MirrorServletTest withParameterSetup(boolean value) {
        when(servletRequest.getParameter("setup")).thenReturn(String.valueOf(value));
        return this;
    }

    private MirrorServletTest withRequestError(String errorMessage, String reason) {
        when(servletRequest.getParameter("error_description")).thenReturn(errorMessage);
        when(servletRequest.getParameter("error")).thenReturn(reason);
        return this;
    }

    private MirrorServletTest withUpstream(String url) {
        when(service.get()).thenReturn(upstream);

        when(upstream.getBaseUrl()).thenReturn(url);
        return this;
    }
}