package com.atlassian.stash.internal.web.users;

import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.permission.PermissionService;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.TestApplicationUser;
import com.atlassian.bitbucket.avatar.CacheableAvatarSupplier;
import com.atlassian.bitbucket.user.UserService;
import com.atlassian.bitbucket.user.NoSuchUserException;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.stash.internal.avatar.InternalAvatarService;
import com.atlassian.stash.internal.project.InternalProjectService;
import com.atlassian.bitbucket.nav.NavBuilder;
import com.atlassian.bitbucket.project.PersonalProject;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.DelegatingServletOutputStream;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasToString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isA;
import static org.mockito.Matchers.same;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.*;

//See CurrentUserProfileControllerTest for a test that covers TIMESTAMP_UNKNOWN handling.
@RunWith(MockitoJUnitRunner.class)
public class UserProfileControllerTest {

    @Mock
    private InternalAvatarService avatarService;
    @Mock
    private AuthenticationContext authenticationContext;
    @InjectMocks
    private UserProfileController controller;
    @Spy
    @SuppressWarnings("unused")
    private I18nService i18nService = new SimpleI18nService();
    @Mock
    private PermissionService permissionService;
    @Mock
    private PersonalProject personalProject;
    @Mock
    private InternalProjectService projectService;
    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
    private NavBuilder navBuilder;
    @Mock
    private UserService userService;

    @Before
    public void setup() {
        when(projectService.getPersonalProject(isA(TestApplicationUser.class))).thenReturn(personalProject);
    }

    //This version of the test tests a CacheableAvatarSupplier with modified less recently than the If-Modified-Since
    //header, which should result in a 304 NOT MODIFIED response.
    @Test
    public void testRetrieveAvatarForNonexistentUser() throws IOException {
        CacheableAvatarSupplier avatarSupplier = mock(CacheableAvatarSupplier.class);
        when(avatarSupplier.getTimestamp()).thenReturn(4000L); //4 seconds into 1970!

        HttpServletRequest request = mock(HttpServletRequest.class);
        when(request.getDateHeader(eq("If-Modified-Since"))).thenReturn(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()));
        when(request.getParameter(eq("v"))).thenReturn(null);

        HttpServletResponse response = mock(HttpServletResponse.class);

        when(avatarService.getUserDefault(eq(256))).thenReturn(avatarSupplier);

        controller.retrieveAvatar("nonexistent", 256, request, response);

        verify(avatarService).getUserDefault(eq(256));
        verify(userService).getUserBySlug(eq("nonexistent"));

        verify(avatarSupplier).getTimestamp();
        verifyNoMoreInteractions(avatarSupplier); //The content type and stream should never be accessed

        verify(request).getDateHeader(eq("If-Modified-Since"));
        verify(request).getParameter(eq("v"));

        verifyNoMoreInteractions(request);

        verify(response).sendError(eq(HttpServletResponse.SC_NOT_MODIFIED));
    }

    //This version of the test tests a CacheableAvatarSupplier a known, non-eternal timestamp results in cache headers
    //being set on the response to allow the avatar to be cached.
    @Test
    public void testRetrieveAvatarSetsHeaders() throws IOException {
        long now = System.currentTimeMillis();

        ApplicationUser user = mock(ApplicationUser.class);
        HttpServletRequest request = mock(HttpServletRequest.class);
        CacheableAvatarSupplier avatarSupplier = mock(CacheableAvatarSupplier.class);
        HttpServletResponse response = mock(HttpServletResponse.class);

        setupMockingForSetHeaders(avatarSupplier, now, request, user);

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        when(response.getOutputStream()).thenReturn(new DelegatingServletOutputStream(outputStream));

        controller.retrieveAvatar("slug", 256, request, response);

        verify(userService).getAvatar(same(user), eq(256));
        verify(userService).getUserBySlug(eq("slug"));

        verify(avatarSupplier).getContentType();
        verify(avatarSupplier).open();
        verify(avatarSupplier).getTimestamp();

        verify(request).getDateHeader(eq("If-Modified-Since"));
        verify(request).getParameter(eq("v"));
        verifyNoMoreInteractions(request);

        verify(response).setDateHeader(eq("Last-Modified"), eq(now));
        verify(response).setContentType(eq("image/tiff"));
        verify(response).getOutputStream();
        verify(response).flushBuffer();
    }

    //This version of the test tests a CacheableAvatarSupplier with eternal caching, which should always result in a
    //304 NOT MODIFIED response.
    @Test
    public void testRetrieveAvatar() throws IOException {
        ApplicationUser user = mock(ApplicationUser.class);

        CacheableAvatarSupplier avatarSupplier = mock(CacheableAvatarSupplier.class);
        when(avatarSupplier.getTimestamp()).thenReturn((long) CacheableAvatarSupplier.TIMESTAMP_ETERNAL);

        HttpServletRequest request = mock(HttpServletRequest.class);
        when(request.getDateHeader(eq("If-Modified-Since"))).thenReturn(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()));
        when(request.getParameter(eq("v"))).thenReturn(null);

        HttpServletResponse response = mock(HttpServletResponse.class);

        when(userService.getAvatar(same(user), eq(256))).thenReturn(avatarSupplier);
        when(userService.getUserBySlug(eq("slug"))).thenReturn(user);

        controller.retrieveAvatar("slug", 256, request, response);

        verify(userService).getAvatar(same(user), eq(256));
        verify(userService).getUserBySlug(eq("slug"));

        verify(avatarSupplier).getTimestamp();
        verifyNoMoreInteractions(avatarSupplier); //The content type and stream should never be accessed

        verify(request).getDateHeader(eq("If-Modified-Since"));
        verify(request).getParameter(eq("v"));
        verifyNoMoreInteractions(request);

        verify(response).sendError(eq(HttpServletResponse.SC_NOT_MODIFIED));
    }

    @Test(expected = NoSuchUserException.class)
    public void testShowProfileForNonexistentUser() {
        when(userService.getUserByName(anyString())).thenReturn(null);
        controller.showProfile("user");
    }

    @Test
    public void testShowProfileForSelf() {
        TestApplicationUser user = new TestApplicationUser("user");

        when(authenticationContext.getCurrentUser()).thenReturn(user);
        when(userService.getUserBySlug("user")).thenReturn(user);

        ModelAndView modelAndView = controller.showProfile("user");
        assertEquals(UserProfileController.OWN_PROFILE_VIEW, modelAndView.getViewName());
    }

    @Test
    public void testShowProfile() {
        when(authenticationContext.getCurrentUser()).thenReturn(new TestApplicationUser("self"));
        when(userService.getUserBySlug("other")).thenReturn(new TestApplicationUser("other"));

        ModelAndView modelAndView = controller.showProfile("other");
        assertEquals(UserProfileController.OTHERS_PROFILE_VIEW, modelAndView.getViewName());
    }

    @Test(expected = NoSuchUserException.class)
    public void testUploadAvatarForNonexistentUser() {
        try {
            controller.uploadAvatar("slug", "data-uri");
        } finally {
            verify(userService, never()).updateAvatar(any(ApplicationUser.class), eq("data-uri"));
        }
    }

    @Test
    public void testUploadAvatar() {
        ApplicationUser user = new TestApplicationUser("user");
        when(userService.getUserBySlug("user")).thenReturn(user);
        when(navBuilder.user(same(user)).avatar(anyInt()).buildAbsolute()).thenReturn("https://stash/users/foo/avatar.png");

        ResponseEntity<?> response = controller.uploadAvatar("user", "data-uri");
        assertThat(response.getStatusCode(), equalTo(HttpStatus.CREATED));
        assertThat(response.getHeaders().getLocation(), hasToString("https://stash/users/foo/avatar.png"));

        verify(userService).getUserBySlug(eq("user"));
        verify(userService).updateAvatar(same(user), eq("data-uri"));
    }

    private void setupMockingForSetHeaders(CacheableAvatarSupplier avatarSupplier, long now, HttpServletRequest request,
                                           ApplicationUser user) throws IOException {

        ByteArrayInputStream inputStream = new ByteArrayInputStream("Test".getBytes());

        when(avatarSupplier.getContentType()).thenReturn("image/tiff");
        when(avatarSupplier.open()).thenReturn(inputStream);
        when(avatarSupplier.getTimestamp()).thenReturn(now);

        when(request.getDateHeader(eq("If-Modified-Since"))).thenReturn(-1L);

        when(userService.getAvatar(same(user), eq(256))).thenReturn(avatarSupplier);
        when(userService.getUserBySlug(eq("slug"))).thenReturn(user);
    }
}
