package com.atlassian.bitbucket.internal.mirroring.mirror.auth.http;

import com.atlassian.bitbucket.auth.ExpiredAuthenticationException;
import com.atlassian.bitbucket.auth.HttpAuthenticationContext;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.internal.mirroring.mirror.InternalUpstreamServer;
import com.atlassian.bitbucket.internal.mirroring.mirror.InternalUpstreamService;
import com.atlassian.bitbucket.internal.mirroring.mirror.MirrorAuthenticationContext;
import com.atlassian.bitbucket.internal.mirroring.mirror.UpstreamUserHelper;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.transaction.SimpleSalTransactionTemplate;
import com.atlassian.bitbucket.user.ServiceUser;
import com.atlassian.bitbucket.user.TestServiceUser;
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 javax.servlet.http.HttpServletRequest;

import static java.util.Optional.empty;
import static java.util.Optional.of;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class UpstreamServerUserAuthenticationHandlerTest {
    @Mock
    private InternalUpstreamService upstreamService;
    @Spy
    private SimpleSalTransactionTemplate transactionTemplate;
    @Spy
    private SimpleI18nService i18nService = new SimpleI18nService();
    @Mock
    private UpstreamUserHelper securityHelper;
    @Mock
    private MirrorAuthenticationContext authenticationContext;
    @Mock
    private HttpAuthenticationContext context;
    @Mock
    private HttpServletRequest request;
    @Mock
    private InternalUpstreamServer upstreamServer;
    @Mock
    private ServiceUser upstreamUser;
    @InjectMocks
    private UpstreamServerUserAuthenticationHandler handler;

    @Before
    public void setUp() throws Exception {
        when(upstreamService.isMirror()).thenReturn(true);
        when(context.getMethod()).thenReturn(HttpAuthenticationContext.METHOD_TOKEN);
        when(context.getRequest()).thenReturn(request);
        when(request.getAttribute("Plugin-Key")).thenReturn("bitbucket.xyz");
        when(authenticationContext.getCurrentUpstreamUser()).thenReturn(empty());
        when(upstreamService.get()).thenReturn(upstreamServer);
        when(upstreamServer.getIssuerId()).thenReturn(of("bitbucket.xyz"));
        when(upstreamServer.getId()).thenReturn("bitbucket.xyz-id");
        when(securityHelper.getUserForUpstream("bitbucket.xyz-id")).thenReturn(upstreamUser);
        when(upstreamUser.isActive()).thenReturn(true);
    }

    @Test(expected = ExpiredAuthenticationException.class)
    public void testValidateExpiresUpstreamAuthInSessionIfJwtSigned() throws Exception {
        ServiceUser upstreamUser = new TestServiceUser("upstream_1234", 0, "nada@nada.com", true);
        when(authenticationContext.getCurrentUpstreamUser()).thenReturn(of(upstreamUser));
        when(request.getAttribute("Plugin-Key")).thenReturn("add-on-key");

        handler.validateAuthentication(context);
    }

    @Test
    public void testValidateIgnoredIfNotJwtSigned() throws Exception {
        ServiceUser upstreamUser = new TestServiceUser("upstream_1234", 0, "nada@nada.com", true);
        when(authenticationContext.getCurrentUpstreamUser()).thenReturn(of(upstreamUser));
        when(request.getAttribute("Plugin-Key")).thenReturn(null);

        handler.validateAuthentication(context);
    }

    @Test
    public void testValidateIgnoredIfNoUpstreamUserAuthenticated() throws Exception {
        handler.validateAuthentication(context);
    }

    @Test
    public void testAuthenticateIgnoredIfNotMirror() throws Exception {
        when(upstreamService.isMirror()).thenReturn(false);

        assertNull(handler.authenticate(context));

        verify(upstreamService).isMirror();
        verifyNoMoreAuthenticateInteractions();
    }

    @Test
    public void testAuthenticateIgnoredIfNonTokenAuthMethod() throws Exception {
        when(context.getMethod()).thenReturn(HttpAuthenticationContext.METHOD_BASIC);

        assertNull(handler.authenticate(context));

        verify(upstreamService).isMirror();
        verify(context).getMethod();
        verifyNoMoreAuthenticateInteractions();
    }

    @Test
    public void testAuthenticationIgnoredIfNonJwtSignedRequest() throws Exception {
        when(request.getAttribute("Plugin-Key")).thenReturn(null);

        assertNull(handler.authenticate(context));

        verify(upstreamService).isMirror();
        verify(context).getMethod();
        verify(context).getRequest();
        verify(request).getAttribute("Plugin-Key");
        verifyNoMoreAuthenticateInteractions();
    }

    @Test
    public void testAuthenticationIgnoredIfNotInstalledUpstream() throws Exception {
        when(upstreamService.get()).thenReturn(null);

        assertNull(handler.authenticate(context));

        verify(upstreamService).isMirror();
        verify(context).getMethod();
        verify(context).getRequest();
        verify(request).getAttribute("Plugin-Key");
        verify(upstreamService).get();
        verifyNoMoreAuthenticateInteractions();
    }

    @Test
    public void testAuthenticationIgnoredIfPluginKeyDoesntMatchUpstream() throws Exception {
        when(upstreamServer.getIssuerId()).thenReturn(of("bitbucket.1234"));

        assertNull(handler.authenticate(context));

        verify(upstreamService).isMirror();
        verify(context).getMethod();
        verify(context).getRequest();
        verify(request).getAttribute("Plugin-Key");
        verify(upstreamService).get();
        verify(upstreamServer).getIssuerId();
        verifyNoMoreAuthenticateInteractions();
    }

    @Test
    public void testAuthenticationIgnoredIfNoUpstreamServiceUser() throws Exception {
        when(securityHelper.getUserForUpstream("bitbucket.xyz-id")).thenReturn(null);

        assertNull(handler.authenticate(context));

        verify(upstreamService).isMirror();
        verify(context).getMethod();
        verify(context).getRequest();
        verify(request).getAttribute("Plugin-Key");
        verify(upstreamService).get();
        verify(upstreamServer).getId();
        verify(upstreamServer).getIssuerId();
        verify(securityHelper).getUserForUpstream("bitbucket.xyz-id");
        verifyNoMoreAuthenticateInteractions();
    }

    @Test
    public void testAuthenticationIgnoredIfUpstreamServiceUserInactive() throws Exception {
        when(upstreamUser.isActive()).thenReturn(false);

        assertNull(handler.authenticate(context));

        verify(upstreamService).isMirror();
        verify(context).getMethod();
        verify(context).getRequest();
        verify(request).getAttribute("Plugin-Key");
        verify(upstreamService).get();
        verify(upstreamServer).getId();
        verify(upstreamServer).getIssuerId();
        verify(securityHelper).getUserForUpstream("bitbucket.xyz-id");
        verify(upstreamUser).isActive();
        verifyNoMoreAuthenticateInteractions();
    }

    @Test
    public void testAuthenticationSucceeds() throws Exception {
        assertSame(upstreamUser, handler.authenticate(context));

        verify(upstreamService).isMirror();
        verify(context).getMethod();
        verify(context, atLeast(1)).getRequest();
        verify(request).getAttribute("Plugin-Key");
        verify(request).getParameter("highest-permission");
        verify(request).setAttribute("bitbucket.jwt.highest.permission", null);
        verify(upstreamService).get();
        verify(upstreamServer).getId();
        verify(upstreamServer).getIssuerId();
        verify(securityHelper).getUserForUpstream("bitbucket.xyz-id");
        verify(upstreamUser).isActive();
        verifyNoMoreAuthenticateInteractions();
    }

    @Test
    public void testAuthenticationSucceedsWithHighestPermission() throws Exception {
        when(request.getParameter("highest-permission")).thenReturn("ADMIN");

        assertSame(upstreamUser, handler.authenticate(context));

        verify(upstreamService).isMirror();
        verify(context).getMethod();
        verify(context, atLeast(1)).getRequest();
        verify(request).getAttribute("Plugin-Key");
        verify(request).getParameter("highest-permission");
        verify(request).setAttribute("bitbucket.jwt.highest.permission", Permission.ADMIN);
        verify(upstreamService).get();
        verify(upstreamServer).getId();
        verify(upstreamServer).getIssuerId();
        verify(securityHelper).getUserForUpstream("bitbucket.xyz-id");
        verify(upstreamUser).isActive();
        verifyNoMoreAuthenticateInteractions();
    }

    @Test
    public void testAuthenticationSucceedsWithInvalidHighestPermission() throws Exception {
        when(request.getParameter("highest-permission")).thenReturn("FOO");

        assertSame(upstreamUser, handler.authenticate(context));

        verify(upstreamService).isMirror();
        verify(context).getMethod();
        verify(context, atLeast(1)).getRequest();
        verify(request).getAttribute("Plugin-Key");
        verify(request).getParameter("highest-permission");
        verify(request).setAttribute("bitbucket.jwt.highest.permission", null);
        verify(upstreamService).get();
        verify(upstreamServer).getId();
        verify(upstreamServer).getIssuerId();
        verify(securityHelper).getUserForUpstream("bitbucket.xyz-id");
        verify(upstreamUser).isActive();
        verifyNoMoreAuthenticateInteractions();
    }

    private void verifyNoMoreAuthenticateInteractions() {
        verifyNoMoreInteractions(upstreamServer, upstreamService, securityHelper, context, request, upstreamUser);
    }
}