package com.atlassian.bitbucket.internal.ssh.auth;

import com.atlassian.bitbucket.auth.*;
import com.atlassian.bitbucket.i18n.KeyedMessage;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.internal.ssh.server.*;
import com.atlassian.bitbucket.scm.AuthenticationState;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.plugin.PluginAccessor;
import com.google.common.collect.Lists;
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 java.io.IOException;
import java.security.PublicKey;
import java.util.List;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class PluginSshAuthenticationHandlerTest {

    private static final String USERNAME = "username";

    private final List<SshAuthenticationHandlerModuleDescriptor> authModuleDescriptors = Lists.newArrayList();
    private final List<SshAuthenticationSuccessHandlerModuleDescriptor> authSuccessModuleDescriptors = Lists.newArrayList();
    private final List<SshAuthenticationFailureHandlerModuleDescriptor> authFailureModuleDescriptors = Lists.newArrayList();

    @InjectMocks
    private PluginSshAuthenticationHandler authenticationHandler;
    @Spy
    private SimpleI18nService i18nService;
    @Mock
    private PluginAccessor pluginAccessor;
    @Mock
    private PublicKey publicKey;
    @Mock
    private ApplicationUser user;

    @Before
    public void setup() {
        when(pluginAccessor.getEnabledModuleDescriptorsByClass(SshAuthenticationHandlerModuleDescriptor.class))
                .thenReturn(authModuleDescriptors);
        when(pluginAccessor.getEnabledModuleDescriptorsByClass(SshAuthenticationSuccessHandlerModuleDescriptor.class))
                .thenReturn(authSuccessModuleDescriptors);
        when(pluginAccessor.getEnabledModuleDescriptorsByClass(SshAuthenticationFailureHandlerModuleDescriptor.class))
                .thenReturn(authFailureModuleDescriptors);
    }

    @Test
    public void testAuthenticate() throws Exception {
        DefaultSshAuthenticationContext context = newAuthContext();

        SshAuthenticationHandler handler = mockAuthenticationHandler(context, user);

        assertSame(user, authenticationHandler.authenticate(context));

        verify(handler).authenticate(context);
    }

    @Test
    public void testAuthenticateStopsOnFirstSuccess() throws Exception {
        DefaultSshAuthenticationContext context = newAuthContext();

        SshAuthenticationHandler handler = mockAuthenticationHandler(context, user);
        ApplicationUser otherUser = mock(ApplicationUser.class);
        SshAuthenticationHandler neverCalledModuleDescriptor = mockAuthenticationHandler(context, otherUser);

        assertSame(user, authenticationHandler.authenticate(context));
        verify(neverCalledModuleDescriptor, never()).authenticate(context);
        verify(handler).authenticate(context);
    }

    @Test
    public void testAuthenticateWithMultipleHandlers() throws Exception {
        DefaultSshAuthenticationContext context = newAuthContext();

        SshAuthenticationHandler handler1 = mockAuthenticationHandler(context, null);
        SshAuthenticationHandler handler2 = mockAuthenticationHandler(context, user);

        assertSame(user, authenticationHandler.authenticate(context));
        verify(handler1).authenticate(context);
        verify(handler2).authenticate(context);
    }

    @Test(expected = IllegalStateException.class)
    public void testAuthenticateHandlersThrowingExceptions() throws Exception {
        DefaultSshAuthenticationContext context = newAuthContext();

        SshAuthenticationHandler handler = mockAuthenticationHandler(context, user);

        when(handler.authenticate(same(context))).thenThrow(new IllegalStateException());

        assertSame(user, authenticationHandler.authenticate(context));
    }

    @Test(expected = SshAuthenticationHandlerNotFoundException.class)
    public void testNoAuthenticateHandlersCanHandle() throws Exception {
        DefaultSshAuthenticationContext context = newAuthContext();

        SshAuthenticationHandler handler = mockAuthenticationHandler(context, user);

        when(handler.authenticate(same(context))).thenReturn(null);

        authenticationHandler.authenticate(context);
    }

    @Test
    public void testOnAuthenticationFailure() throws Exception {
        DefaultSshAuthenticationFailureContext context = newFailureContext();

        SshAuthenticationFailureHandler handler = mockAuthenticationFailureHandler();

        authenticationHandler.onAuthenticationFailure(context);

        verify(handler).onAuthenticationFailure(same(context));
    }

    @Test
    public void testOnAuthenticationFailureWithMultipleHandlers() throws Exception {
        DefaultSshAuthenticationFailureContext context = newFailureContext();

        SshAuthenticationFailureHandler handler1 = mockAuthenticationFailureHandler();
        SshAuthenticationFailureHandler handler2 = mockAuthenticationFailureHandler();

        authenticationHandler.onAuthenticationFailure(context);

        verify(handler1).onAuthenticationFailure(same(context));
        verify(handler2).onAuthenticationFailure(same(context));
    }

    @Test
    public void testOnAuthenticationFailureWithMultipleHandlersAndTheFirstThrowingAnException() throws Exception {
        DefaultSshAuthenticationFailureContext context = newFailureContext();

        SshAuthenticationFailureHandler handler1 = mockAuthenticationFailureHandler();
        SshAuthenticationFailureHandler handler2 = mockAuthenticationFailureHandler();

        doThrow(new RuntimeException()).when(handler1).onAuthenticationFailure(same(context));

        authenticationHandler.onAuthenticationFailure(context);

        verify(handler1).onAuthenticationFailure(same(context));
        verify(handler2).onAuthenticationFailure(same(context));
    }

    @Test
    public void testOnAuthenticationSuccess() throws Exception {
        DefaultSshAuthenticationSuccessContext context = newSuccessContext();

        SshAuthenticationSuccessHandler handler = mockAuthenticationSuccessHandler();

        authenticationHandler.onAuthenticationSuccess(context);

        verify(handler).onAuthenticationSuccess(same(context));
    }

    @Test
    public void testOnAuthenticationSuccessWithMultipleHandlers() throws Exception {
        DefaultSshAuthenticationSuccessContext context = newSuccessContext();

        SshAuthenticationSuccessHandler handler1 = mockAuthenticationSuccessHandler();
        SshAuthenticationSuccessHandler handler2 = mockAuthenticationSuccessHandler();

        authenticationHandler.onAuthenticationSuccess(context);

        verify(handler1).onAuthenticationSuccess(same(context));
        verify(handler2).onAuthenticationSuccess(same(context));
    }

    @Test
    public void testOnAuthenticationSuccessWithMultipleHandlersAndTheFirstThrowingAnException() throws Exception {
        DefaultSshAuthenticationSuccessContext context = newSuccessContext();

        SshAuthenticationSuccessHandler handler1 = mockAuthenticationSuccessHandler();
        SshAuthenticationSuccessHandler handler2 = mockAuthenticationSuccessHandler();

        doThrow(new RuntimeException()).when(handler1).onAuthenticationSuccess(same(context));

        authenticationHandler.onAuthenticationSuccess(context);

        verify(handler1).onAuthenticationSuccess(same(context));
        verify(handler2).onAuthenticationSuccess(same(context));
    }

    @Test
    public void testHasFailureHandlers() throws Exception {
        assertFalse(authenticationHandler.hasFailureHandlers());

        SshAuthenticationFailureHandler pluginHandler = mock(SshAuthenticationFailureHandler.class);

        SshAuthenticationFailureHandlerModuleDescriptor moduleDescriptor = mock(SshAuthenticationFailureHandlerModuleDescriptor.class);
        when(moduleDescriptor.getModule()).thenReturn(pluginHandler);

        authFailureModuleDescriptors.add(moduleDescriptor);

        assertTrue(authenticationHandler.hasFailureHandlers());
    }

    private SshAuthenticationFailureHandler mockAuthenticationFailureHandler() throws IOException {
        SshAuthenticationFailureHandler pluginHandler = mock(SshAuthenticationFailureHandler.class);

        SshAuthenticationFailureHandlerModuleDescriptor moduleDescriptor = mock(SshAuthenticationFailureHandlerModuleDescriptor.class);
        when(moduleDescriptor.getModule()).thenReturn(pluginHandler);

        authFailureModuleDescriptors.add(moduleDescriptor);

        return pluginHandler;
    }

    private SshAuthenticationHandler mockAuthenticationHandler(SshAuthenticationContext authenticationContext,
                                                               ApplicationUser user) {
        SshAuthenticationHandler pluginHandler = mock(SshAuthenticationHandler.class);
        when(pluginHandler.authenticate(authenticationContext)).thenReturn(user);

        SshAuthenticationHandlerModuleDescriptor moduleDescriptor = mock(SshAuthenticationHandlerModuleDescriptor.class);
        when(moduleDescriptor.getModule()).thenReturn(pluginHandler);

        authModuleDescriptors.add(moduleDescriptor);

        return pluginHandler;
    }

    private SshAuthenticationSuccessHandler mockAuthenticationSuccessHandler() throws IOException {
        SshAuthenticationSuccessHandler pluginHandler = mock(SshAuthenticationSuccessHandler.class);

        SshAuthenticationSuccessHandlerModuleDescriptor moduleDescriptor = mock(SshAuthenticationSuccessHandlerModuleDescriptor.class);
        when(moduleDescriptor.getModule()).thenReturn(pluginHandler);

        authSuccessModuleDescriptors.add(moduleDescriptor);

        return pluginHandler;
    }

    private DefaultSshAuthenticationContext newAuthContext() {
        return new DefaultSshAuthenticationContext(new SshCredentials(USERNAME, publicKey));
    }

    private DefaultSshAuthenticationFailureContext newFailureContext() {
        return new DefaultSshAuthenticationFailureContext(new SshCredentials(USERNAME, publicKey),
                AuthenticationState.NOT_AUTHENTICATED, new AuthenticationSystemException(new KeyedMessage("", "", "")));
    }

    private DefaultSshAuthenticationSuccessContext newSuccessContext() {
        return new DefaultSshAuthenticationSuccessContext(new SshAuthentication(
                user, new SshCredentials(USERNAME, publicKey), new SshKeyDetails(null, null)), "whoami");
    }
}