package com.atlassian.stash.internal.auth;

import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.stash.internal.concurrent.TransferableState;
import com.atlassian.stash.internal.user.InternalApplicationUser;
import com.atlassian.stash.internal.user.InternalNormalUser;
import com.atlassian.stash.internal.user.PermissionGraph;
import com.atlassian.stash.internal.user.StashUserAuthenticationToken;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;

import java.util.Locale;

import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.*;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;

public class DefaultAuthenticationContextTest {

    private Authentication authentication;
    private final DefaultAuthenticationContext authenticationContext = new DefaultAuthenticationContext();

    @Before
    public void setUp() throws Exception {
        authentication = SecurityContextHolder.getContext().getAuthentication();
    }

    @After
    public void tearDown() throws Exception {
        SecurityContextHolder.getContext().setAuthentication(authentication);
    }

    @Test
    public void testAnonymous() throws Exception {
        SecurityContextHolder.getContext().setAuthentication(null);

        assertNull(authenticationContext.getCurrentToken());
        assertNull(authenticationContext.getCurrentUser());
        assertFalse(authenticationContext.isAuthenticated());
    }

    @Test
    public void testLogged() throws Exception {
        final InternalApplicationUser user = normalUser("test");
        StashUserAuthenticationToken token = StashUserAuthenticationToken.forUser(user);
        SecurityContextHolder.getContext().setAuthentication(token);

        assertSame(token, authenticationContext.getCurrentToken());
        assertEquals(user, authenticationContext.getCurrentUser());
        assertTrue(authenticationContext.isAuthenticated());
    }

    @Test
    public void testPromotedAuthentication() throws Exception {
        PermissionGraph elevatedPermissions = mock(PermissionGraph.class);
        StashUserAuthenticationToken token = new StashUserAuthenticationToken.Builder()
                .elevatedPermissions(elevatedPermissions)
                .build();
        SecurityContextHolder.getContext().setAuthentication(token);

        assertSame(token, authenticationContext.getCurrentToken());
        assertNull(authenticationContext.getCurrentUser());
        assertFalse(authenticationContext.isAuthenticated());
    }

    @Test
    public void testGetStateGenericAuth() throws Exception {
        //makes sure that any Authentication used by spring is preserved by the stateful service
        testAuthenticationClass(Authentication.class);
    }

    @Test
    public void testGetStateStashAuth() throws Exception {
        //makes sure that Stash specific Authentication is preserved by the stateful service
        testAuthenticationClass(StashUserAuthenticationToken.class);
    }

    @Test(expected = NullPointerException.class)
    public void testRefreshUserDetailsThrowsNPEOnNullUser() {
        authenticationContext.refreshUser(null);
    }

    @Test(expected = IllegalStateException.class)
    public void testRefreshUserDetailsThrowsIllegalWhenNoCurrentUser() {
        withNoUser();

        authenticationContext.refreshUser(normalUser("user"));
    }

    @Test(expected = IllegalArgumentException.class)
    public void testRefreshUserDetailsThrowsIAEWhenPassedUserNotSameAsCurrentUser() {
        withCurrentUser(normalUser("user", 0, Locale.ENGLISH));

        authenticationContext.refreshUser(normalUser("otherUser", 1, Locale.FRANCE));
    }

    @Test
    public void testRefreshUserDetailsUpdatesDetailsWhenUserIdMatches() {
        withCurrentUser(normalUser("user", 0, Locale.ENGLISH));

        authenticationContext.refreshUser(normalUser("other", 0, Locale.FRANCE));

        InternalNormalUser currentUser = (InternalNormalUser) authenticationContext.getCurrentUser();
        assertThat(currentUser, notNullValue());
        assertThat(currentUser.getName(), equalTo("other"));
        assertThat(currentUser.getId(), equalTo(0));
        assertThat(currentUser.getLocale(), equalTo(Locale.FRANCE));
    }

    private static InternalNormalUser normalUser(String name) {
        return normalUser(name, 0, Locale.ENGLISH);
    }

    private static InternalNormalUser normalUser(String name, int id, Locale locale) {
        return new InternalNormalUser.Builder().name(name).id(id).locale(locale).build();
    }

    public void withCurrentUser(ApplicationUser user) {
        SecurityContextHolder.getContext().setAuthentication(StashUserAuthenticationToken.forUser(user));
    }

    public void withNoUser() {
        SecurityContextHolder.getContext().setAuthentication(null);
    }

    private void testAuthenticationClass(Class<? extends Authentication> authenticationClass) {
        SecurityContext oldContext = SecurityContextHolder.getContext();
        try {
            SecurityContext context = mock(SecurityContext.class);
            SecurityContextHolder.setContext(context);

            Authentication auth = mock(authenticationClass);
            when(context.getAuthentication()).thenReturn(auth);

            DefaultAuthenticationContext service = new DefaultAuthenticationContext();

            TransferableState state = service.getState();

            state.apply();

            verify(context).setAuthentication(eq(auth));

            state.remove();

            assertNotSame(auth, SecurityContextHolder.getContext());

        } finally {
            if (oldContext != null) {
                SecurityContextHolder.setContext(oldContext);
            }
        }
    }

}
