package com.atlassian.bitbucket.internal.crowd.sso;

import com.atlassian.cache.memory.MemoryCacheManager;
import com.atlassian.crowd.embedded.api.*;
import com.atlassian.crowd.event.directory.DirectoryCreatedEvent;
import com.atlassian.crowd.event.directory.DirectoryDeletedEvent;
import com.atlassian.crowd.event.directory.DirectoryUpdatedEvent;
import com.atlassian.crowd.exception.OperationFailedException;
import com.atlassian.crowd.integration.rest.service.factory.RestCrowdClientFactory;
import com.atlassian.crowd.model.authentication.CookieConfiguration;
import com.atlassian.crowd.service.client.ClientProperties;
import com.atlassian.crowd.service.client.CrowdClient;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.nav.NavBuilder;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
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.http.HttpServletRequest;
import java.util.Arrays;

import static org.junit.Assert.*;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class DefaultCrowdSsoServiceTest {

    @Mock
    private CookieConfiguration cookieConfigurationWithDomain;
    @Mock
    private CookieConfiguration cookieConfigurationWithoutDomain;
    @Mock
    private CrowdClient crowdClient;
    @Mock
    private CrowdService crowdService;
    @Mock
    private CrowdDirectoryService directoryService;
    private I18nService i18nService = new SimpleI18nService();
    private NavBuilder navBuilder;
    @Mock
    private ApplicationPropertiesService propertiesService;
    @Mock
    private RestCrowdClientFactory restCrowdClientFactory;

    @Before
    public void setup() {
        navBuilder = mock(NavBuilder.class, RETURNS_DEEP_STUBS);
        when(navBuilder.login().buildConfigured()).thenReturn("/login");
        when(restCrowdClientFactory.newInstance(any(ClientProperties.class))).thenReturn(crowdClient);
        when(cookieConfigurationWithDomain.getDomain()).thenReturn(".domain.com");
        when(cookieConfigurationWithDomain.getName()).thenReturn("crowd_token");
        when(cookieConfigurationWithoutDomain.getName()).thenReturn("crowd_token");
        when(propertiesService.getPluginProperty(eq(DefaultCrowdSsoService.PROP_CONFIG_ERROR_WAIT), anyInt())).thenReturn(2);
        when(propertiesService.getPluginProperty(eq(DefaultCrowdSsoService.PROP_CONFIG_TTL), anyInt())).thenReturn(10);
    }

    @Test
    public void testIsAvailableForCrowdDirectory() throws Exception {
        DefaultCrowdSsoService service = createService(true, mockDirectory(DirectoryType.CROWD, 1L, true));
        when(crowdClient.getCookieConfiguration()).thenReturn(cookieConfigurationWithDomain);

        assertTrue(service.isAvailable());
        assertNotNull(service.getAuthenticator());
        verify(crowdClient).getCookieConfiguration();
    }

    @Test
    public void testIsAvailableForCrowdDirectoryNoDomain() throws Exception {
        DefaultCrowdSsoService service = createService(true, mockDirectory(DirectoryType.CROWD, 1L, true));
        when(crowdClient.getCookieConfiguration()).thenReturn(cookieConfigurationWithoutDomain);

        assertTrue(service.isAvailable());
        assertNotNull(service.getAuthenticator());
        verify(crowdClient).getCookieConfiguration();
    }

    @Test
    public void testIsAvailableForCrowdDirectoryWithoutDomain() throws Exception {
        DefaultCrowdSsoService service = createService(true, mockDirectory(DirectoryType.CROWD, 1L, true));
        when(crowdClient.getCookieConfiguration()).thenReturn(cookieConfigurationWithoutDomain);

        assertTrue(service.isAvailable());
        assertNotNull(service.getAuthenticator());
        verify(crowdClient).getCookieConfiguration();
    }

    @Test
    public void testIsAvailableForDisabledCrowdDirectory() throws Exception {
        DefaultCrowdSsoService service = createService(true, mockDirectory(DirectoryType.CROWD, 1L, false));

        assertFalse(service.isAvailable());
        assertNull(service.getAuthenticator());
    }

    @Test
    public void testIsAvailableForInternalDirectory() throws Exception {
        DefaultCrowdSsoService service = createService(true, mockDirectory(DirectoryType.INTERNAL, 1L, true));

        assertFalse(service.isAvailable());
        assertNull(service.getAuthenticator());
    }

    @Test
    public void testIsAvailableForJiraDirectory() throws Exception {
        // Jira looks like a Crowd directory, but does not support SSO
        DefaultCrowdSsoService service = createService(true, mockDirectory(DirectoryType.CROWD, 1L, true));
        when(crowdClient.getCookieConfiguration()).thenThrow(new OperationFailedException());

        assertFalse(service.isAvailable());
    }

    @Test
    public void testIsAvailableThroughProperty() throws Exception {
        DefaultCrowdSsoService service = createService(false, mockDirectory(DirectoryType.CROWD, 1L, true));
        when(crowdClient.getCookieConfiguration()).thenReturn(cookieConfigurationWithDomain);

        assertFalse(service.isAvailable());
        verify(crowdClient, never()).getCookieConfiguration();
    }

    @Test
    public void testIsManagedBySso() throws Exception {
        DefaultCrowdSsoService service = createService(true,
                mockDirectory(DirectoryType.INTERNAL, 1L, true),
                mockDirectory(DirectoryType.CROWD, 2L, true));

        when(crowdClient.getCookieConfiguration()).thenReturn(cookieConfigurationWithDomain);
        mockUser("crowdUser", 2L);
        mockUser("internalUser", 1L);

        assertTrue(service.isManagedBySso("crowdUser"));
        assertFalse(service.isManagedBySso("internalUser"));
    }

    @Test
    public void testIsPossiblySsoDomain() throws Exception {
        // cookie domain is configured as .domain.com
        DefaultCrowdSsoService service = createService(true, mockDirectory(DirectoryType.CROWD, 1L, true));
        when(crowdClient.getCookieConfiguration()).thenReturn(cookieConfigurationWithDomain);

        HttpServletRequest request = mock(HttpServletRequest.class);

        // stash.domain.com should match the cookie domain
        when(request.getServerName()).thenReturn("stash.domain.com");
        assertTrue(service.isSsoCandidate(request));

        // google.com should not
        when(request.getServerName()).thenReturn("stash.google.com");
        assertFalse(service.isSsoCandidate(request));
    }

    @Test
    public void testIsPossiblySsoDomainNoDomainConfigured() throws Exception {
        DefaultCrowdSsoService service = createService(true, mockDirectory(DirectoryType.CROWD, 1L, true));
        CookieConfiguration config = mock(CookieConfiguration.class);
        when(config.getName()).thenReturn("crowd_token");
        when(crowdClient.getCookieConfiguration()).thenReturn(config);

        HttpServletRequest request = mock(HttpServletRequest.class);
        when(request.getServerName()).thenReturn("stash.domain.com");
        assertTrue(service.isSsoCandidate(request));
    }

    @Test
    public void testOnDirectoryEvents() throws Exception {
        Directory internal = mockDirectory(DirectoryType.INTERNAL, 1L, true);
        Directory crowd = mockDirectory(DirectoryType.CROWD, 2L, true);
        DefaultCrowdSsoService service = createService(true, internal, crowd);
        when(crowdClient.getCookieConfiguration()).thenReturn(cookieConfigurationWithDomain);

        assertTrue(service.isAvailable());

        // simulate the crowd dir getting disabled
        when(crowd.isActive()).thenReturn(false);
        service.onDirectoryUpdatedEvent(mock(DirectoryUpdatedEvent.class));
        assertFalse(service.isAvailable());

        // simulate the directory getting reenabled
        when(crowd.isActive()).thenReturn(true);
        service.onDirectoryUpdatedEvent(mock(DirectoryUpdatedEvent.class));
        assertTrue(service.isAvailable());

        // simulate the crowd dir getting deleted
        when(directoryService.findAllDirectories()).thenReturn(Lists.newArrayList(internal));
        service.onDirectoryDeleted(mock(DirectoryDeletedEvent.class));
        assertFalse(service.isAvailable());

        // simulate the crowd dir getting re-added
        when(directoryService.findAllDirectories()).thenReturn(Lists.newArrayList(internal, crowd));
        service.onDirectoryCreated(mock(DirectoryCreatedEvent.class));
        assertTrue(service.isAvailable());
    }

    private DefaultCrowdSsoService createService(boolean enabled, Directory... directories) throws Exception {
        when(propertiesService.getPluginProperty(eq(DefaultCrowdSsoService.PROP_ENABLED))).thenReturn(Boolean.toString(enabled));
        when(directoryService.findAllDirectories()).thenReturn(Arrays.asList(directories));

        return new DefaultCrowdSsoService(new MemoryCacheManager(),
                crowdService, directoryService, i18nService, navBuilder,
                propertiesService, restCrowdClientFactory);
    }

    private Directory mockDirectory(DirectoryType dirType, long id, boolean active) {
        Directory dir = mock(Directory.class);
        when(dir.getType()).thenReturn(dirType);
        when(dir.getId()).thenReturn(id);
        when(dir.isActive()).thenReturn(active);
        if (dirType == DirectoryType.CROWD) {
            when(dir.getAttributes()).thenReturn(
                    ImmutableMap.<String, String>builder()
                            .put("application.name", "Stash")
                            .put("application.password", "pwd")
                            .put("crowd.server.url", "http://crowd.domain.com")
                            .build());
        }

        return dir;
    }

    private void mockUser(String username, long directoryId) {
        User user = mock(User.class);
        when(user.getName()).thenReturn(username);
        when(user.getDirectoryId()).thenReturn(directoryId);
        when(crowdService.getUser(eq(username))).thenReturn(user);
    }
}
