package com.fsck.k9.mail.store.webdav;

import com.fsck.k9.mail.FetchProfile;
import com.fsck.k9.mail.K9LibRobolectricTestRunner;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessageRetrievalListener;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.QMailHttpClient;
import com.fsck.k9.mail.internet.BinaryTempFileBody;
import com.fsck.k9.mail.store.StoreConfig;

import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.BasicHttpEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.protocol.HttpContext;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import static java.util.Collections.singletonList;

import static com.fsck.k9.mail.Folder.OPEN_MODE_RW;
import static com.fsck.k9.mail.Folder.OPEN_MODE_RO;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyMapOf;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import static org.junit.Assert.assertEquals;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@SuppressWarnings("deprecation")
@RunWith(K9LibRobolectricTestRunner.class)
public class WebDavFolderTest {
    @Mock
    private MessageRetrievalListener<WebDavMessage> listener;
    @Mock
    private WebDavStore mockStore;
    @Mock
    private DataSet mockDataSet;
    @Mock
    private QMailHttpClient mockHttpClient;
    @Mock
    private StoreConfig mockStoreConfig;
    @Mock
    private HttpResponse mockHttpResponse;
    @Mock
    private StatusLine mockStatusLine;
    @Captor
    private ArgumentCaptor<Map<String, String>> headerCaptor;
    @Captor
    private ArgumentCaptor<String> urlCaptor;
    @Captor
    private ArgumentCaptor<StringEntity> entityCaptor;

    private WebDavFolder folder;

    private WebDavFolder destinationFolder;
    private String storeUrl = "https://localhost/webDavStoreUrl";
    private String folderName = "testFolder";
    private String moveOrCopyXml = "<xml>MoveOrCopyXml</xml>";
    private HashMap<String, String> moveOrCopyHeaders;
    private List<WebDavMessage> messages;

    @Before
    public void before() throws MessagingException, IOException {
        MockitoAnnotations.initMocks(this);
        when(mockStore.getUrl()).thenReturn(storeUrl);
        when(mockStore.getHttpClient()).thenReturn(mockHttpClient);
        when(mockStore.getStoreConfig()).thenReturn(mockStoreConfig);
        folder = new WebDavFolder(mockStore, folderName);

        setupTempDirectory();
    }

    private void setupTempDirectory() {
        File tempDirectory = new File("temp");
        if (!tempDirectory.exists()) {
            assertTrue(tempDirectory.mkdir());
            tempDirectory.deleteOnExit();
        }
        BinaryTempFileBody.setTempDirectory(tempDirectory);
    }

    private WebDavFolder setupDestinationFolder() {
        WebDavFolder destinationFolder = new WebDavFolder(mockStore, "destFolder");
        when(mockStore.getFolder("destFolder")).thenReturn(destinationFolder);
        return destinationFolder;
    }

    private void setupFolderWithMessages(int count) throws MessagingException {
        HashMap<String, String> headers = new HashMap<>();
        headers.put("Brief", "t");
        String messageCountXml = "<xml>MessageCountXml</xml>";
        when(mockStore.getMessageCountXml("True")).thenReturn(messageCountXml);
        when(mockStore.processRequest("https://localhost/webDavStoreUrl/testFolder",
                "SEARCH", messageCountXml, headers)).thenReturn(mockDataSet);
        when(mockDataSet.getMessageCount()).thenReturn(count);
        folder.getMessageCount();
    }

    private WebDavMessage createWebDavMessage(String uid) {
        WebDavMessage webDavMessage = mock(WebDavMessage.class);
        when(webDavMessage.getUid()).thenReturn(uid);
        return webDavMessage;
    }

    private void setupGetUrlsRequestResponse(String uid, String url) throws MessagingException {
        String getUrlsXml = "<xml>GetUrls</xml>";
        when(mockStore.getMessageUrlsXml(new String[]{uid})).thenReturn(getUrlsXml);
        HashMap<String, String> headers = new HashMap<>();
        headers.put("Brief", "t");
        when(mockStore.processRequest("https://localhost/webDavStoreUrl/testFolder", "SEARCH", getUrlsXml, headers))
                .thenReturn(mockDataSet);
        Map<String, String> urlUids = new HashMap<>();
        urlUids.put(uid, url);
        when(mockDataSet.getUidToUrl()).thenReturn(urlUids);
    }

    @Test
    public void folder_can_fetch_less_than_10_envelopes() throws MessagingException {
        when(mockStore.processRequest(anyString(), anyString(), anyString(), anyMapOf(String.class, String.class)))
                .thenReturn(mockDataSet);

        List<WebDavMessage> messages = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            WebDavMessage mockMessage = mock(WebDavMessage.class);
            messages.add(mockMessage);
        }

        FetchProfile profile = new FetchProfile();
        profile.add(FetchProfile.Item.ENVELOPE);
        folder.fetch(messages, profile, listener);
    }

    @Test
    public void folder_can_fetch_more_than_10_envelopes() throws MessagingException {
        when(mockStore.processRequest(anyString(), anyString(), anyString(),
                anyMapOf(String.class, String.class)))
                .thenReturn(mockDataSet);

        List<WebDavMessage> messages = new ArrayList<>();
        for (int i = 0; i < 15; i++) {
            WebDavMessage mockMessage = mock(WebDavMessage.class);
            messages.add(mockMessage);
        }
        FetchProfile profile = new FetchProfile();
        profile.add(FetchProfile.Item.ENVELOPE);
        folder.fetch(messages, profile, listener);
    }

    @Test
    public void folder_can_fetch_less_than_20_flags() throws MessagingException {
        when(mockStore.processRequest(anyString(), anyString(), anyString(),
                anyMapOf(String.class, String.class)))
                .thenReturn(mockDataSet);

        List<WebDavMessage> messages = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            WebDavMessage mockMessage = mock(WebDavMessage.class);
            messages.add(mockMessage);
        }
        FetchProfile profile = new FetchProfile();
        profile.add(FetchProfile.Item.FLAGS);
        folder.fetch(messages, profile, listener);
    }

    @Test
    public void folder_can_fetch_more_than_20_flags() throws MessagingException {
        when(mockStore.processRequest(anyString(), anyString(), anyString(),
                anyMapOf(String.class, String.class)))
                .thenReturn(mockDataSet);

        List<WebDavMessage> messages = new ArrayList<>();
        for (int i = 0; i < 25; i++) {
            WebDavMessage mockMessage = mock(WebDavMessage.class);
            messages.add(mockMessage);
        }

        FetchProfile profile = new FetchProfile();
        profile.add(FetchProfile.Item.FLAGS);
        folder.fetch(messages, profile, listener);
    }

    @Test
    public void folder_can_fetch_sensible_body_data_and_notifies_listener()
            throws MessagingException, IOException, URISyntaxException {
        setupStoreForMessageFetching();
        List<WebDavMessage> messages = setup25MessagesToFetch();

        when(mockHttpClient.executeOverride(any(HttpUriRequest.class), any(HttpContext.class))).thenAnswer(
                new Answer<HttpResponse>() {
                    @Override
                    public HttpResponse answer(InvocationOnMock invocation) throws Throwable {
                        HttpResponse httpResponse = mock(HttpResponse.class);
                        StatusLine statusLine = mock(StatusLine.class);
                        when(httpResponse.getStatusLine()).thenReturn(statusLine);
                        when(statusLine.getStatusCode()).thenReturn(200);

                        BasicHttpEntity httpEntity = new BasicHttpEntity();
                        String body = "";
                        httpEntity.setContent(new ByteArrayInputStream(body.getBytes("UTF-8")));
                        when(httpResponse.getEntity()).thenReturn(httpEntity);
                        return httpResponse;
                    }
                });

        FetchProfile profile = new FetchProfile();
        profile.add(FetchProfile.Item.BODY_SANE);
        folder.fetch(messages, profile, listener);
        verify(listener, times(25)).messageStarted(any(String.class), anyInt(), eq(25));
        verify(listener, times(25)).messageFinished(any(WebDavMessage.class), anyInt(), eq(25));
    }

    @Test
    public void folder_does_not_notify_listener_twice_when_fetching_flags_and_bodies()
            throws MessagingException, IOException, URISyntaxException {
        setupStoreForMessageFetching();
        when(mockStore.processRequest(anyString(), anyString(), anyString(),
                anyMapOf(String.class, String.class)))
                .thenReturn(mockDataSet);
        List<WebDavMessage> messages = setup25MessagesToFetch();
        when(mockHttpClient.executeOverride(any(HttpUriRequest.class), any(HttpContext.class))).thenAnswer(
                new Answer<HttpResponse>() {
                    @Override
                    public HttpResponse answer(InvocationOnMock invocation) throws Throwable {
                        HttpResponse httpResponse = mock(HttpResponse.class);
                        StatusLine statusLine = mock(StatusLine.class);
                        when(httpResponse.getStatusLine()).thenReturn(statusLine);
                        when(statusLine.getStatusCode()).thenReturn(200);

                        BasicHttpEntity httpEntity = new BasicHttpEntity();
                        String body = "";
                        httpEntity.setContent(new ByteArrayInputStream(body.getBytes("UTF-8")));
                        when(httpResponse.getEntity()).thenReturn(httpEntity);
                        return httpResponse;
                    }
                });

        FetchProfile profile = new FetchProfile();
        profile.add(FetchProfile.Item.FLAGS);
        profile.add(FetchProfile.Item.BODY);
        folder.fetch(messages, profile, listener);
        verify(listener, times(25)).messageStarted(any(String.class), anyInt(), anyInt());
        verify(listener, times(25)).messageFinished(any(WebDavMessage.class), anyInt(), anyInt());
    }

    private void setupStoreForMessageFetching() {
        String authString = "authString";
        when(mockStoreConfig.getMaximumAutoDownloadMessageSize()).thenReturn(1900);
        when(mockStore.getAuthentication()).thenReturn(WebDavConstants.AUTH_TYPE_BASIC);
        when(mockStore.getAuthString()).thenReturn(authString);
    }

    private List<WebDavMessage> setup25MessagesToFetch() {

        List<WebDavMessage> messages = new ArrayList<>();
        for (int i = 0; i < 25; i++) {
            WebDavMessage message = new WebDavMessage("message" + i, folder);
            message.setUrl("http://example.org/Exchange/user/Inbox/message" + i + ".EML");
            messages.add(message);
        }
        return messages;
    }

    @Test
    public void folder_can_handle_empty_response_to_body_request() throws MessagingException, IOException {
        setupStoreForMessageFetching();
        List<WebDavMessage> messages = setup25MessagesToFetch();

        when(mockHttpClient.executeOverride(any(HttpUriRequest.class), any(HttpContext.class))).thenAnswer(
                new Answer<HttpResponse>() {
                    @Override
                    public HttpResponse answer(InvocationOnMock invocation) throws Throwable {
                        HttpResponse httpResponse = mock(HttpResponse.class);
                        StatusLine statusLine = mock(StatusLine.class);
                        when(httpResponse.getStatusLine()).thenReturn(statusLine);
                        when(statusLine.getStatusCode()).thenReturn(200);
                        return httpResponse;
                    }
                });

        FetchProfile profile = new FetchProfile();
        profile.add(FetchProfile.Item.BODY_SANE);
        folder.fetch(messages, profile, listener);
        verify(listener, times(25)).messageStarted(any(String.class), anyInt(), eq(25));
        verify(listener, times(25)).messageFinished(any(WebDavMessage.class), anyInt(), eq(25));
    }

    @Test
    public void folder_ignores_exception_thrown_when_closing() throws MessagingException, IOException {
        setupStoreForMessageFetching();
        List<WebDavMessage> messages = setup25MessagesToFetch();

        when(mockHttpClient.executeOverride(any(HttpUriRequest.class), any(HttpContext.class))).thenAnswer(
                new Answer<HttpResponse>() {
                    @Override
                    public HttpResponse answer(InvocationOnMock invocation) throws Throwable {
                        HttpResponse httpResponse = mock(HttpResponse.class);
                        StatusLine statusLine = mock(StatusLine.class);
                        when(httpResponse.getStatusLine()).thenReturn(statusLine);
                        when(statusLine.getStatusCode()).thenReturn(200);

                        BasicHttpEntity httpEntity = new BasicHttpEntity();
                        InputStream mockInputStream = mock(InputStream.class);
                        when(mockInputStream.read(any(byte[].class), anyInt(), anyInt())).thenReturn(1).thenReturn(-1);
                        doThrow(new IOException("Test")).when(mockInputStream).close();
                        httpEntity.setContent(mockInputStream);
                        when(httpResponse.getEntity()).thenReturn(httpEntity);
                        return httpResponse;
                    }
                });

        FetchProfile profile = new FetchProfile();
        profile.add(FetchProfile.Item.BODY_SANE);
        folder.fetch(messages, profile, listener);
        verify(listener, times(25)).messageStarted(any(String.class), anyInt(), eq(25));
        verify(listener, times(25)).messageFinished(any(WebDavMessage.class), anyInt(), eq(25));
    }

    @Test
    public void folder_does_not_start_open() throws MessagingException {
        assertFalse(folder.isOpen());
    }

    @Test
    public void open_should_open_folder() throws MessagingException {
        folder.open(OPEN_MODE_RW);
        assertTrue(folder.isOpen());
    }

    @Test
    public void close_should_close_folder() throws MessagingException {
        folder.close();
        assertFalse(folder.isOpen());
    }

    @Test
    public void mode_is_always_readwrite() throws Exception {
        assertEquals(OPEN_MODE_RW, folder.getMode());
        folder.open(OPEN_MODE_RO);
        assertEquals(OPEN_MODE_RW, folder.getMode());
    }

    @Test
    public void exists_is_always_true() throws Exception {
        assertTrue(folder.exists());
    }

    @Test
    public void can_fetch_message_count() throws Exception {
        int messageCount = 23;
        HashMap<String, String> headers = new HashMap<>();
        headers.put("Brief", "t");
        String messageCountXml = "<xml>MessageCountXml</xml>";
        when(mockStore.getMessageCountXml("True")).thenReturn(messageCountXml);
        when(mockStore.processRequest("https://localhost/webDavStoreUrl/testFolder",
                "SEARCH", messageCountXml, headers)).thenReturn(mockDataSet);
        when(mockDataSet.getMessageCount()).thenReturn(messageCount);

        int result = folder.getMessageCount();

        assertEquals(messageCount, result);
    }

    @Test
    public void can_fetch_unread_message_count() throws Exception {
        int unreadMessageCount = 13;
        HashMap<String, String> headers = new HashMap<>();
        headers.put("Brief", "t");
        String messageCountXml = "<xml>MessageCountXml</xml>";
        when(mockStore.getMessageCountXml("False")).thenReturn(messageCountXml);
        when(mockStore.processRequest("https://localhost/webDavStoreUrl/testFolder",
                "SEARCH", messageCountXml, headers)).thenReturn(mockDataSet);
        when(mockDataSet.getMessageCount()).thenReturn(unreadMessageCount);

        int result = folder.getUnreadMessageCount();

        assertEquals(unreadMessageCount, result);
    }

    @Test
    public void getMessages_should_request_message_search() throws MessagingException {
        int totalMessages = 23;
        int messageStart = 1;
        int messageEnd = 11;
        setupFolderWithMessages(totalMessages);
        String messagesXml = "<xml>MessagesXml</xml>";
        buildSearchResponse(mockDataSet);
        when(mockStore.getMessagesXml()).thenReturn(messagesXml);
        when(mockStore.processRequest(eq("https://localhost/webDavStoreUrl/testFolder"), eq("SEARCH"),
                eq(messagesXml), Matchers.<Map<String, String>>any())).thenReturn(mockDataSet);

        folder.getMessages(messageStart, messageEnd, new Date(), listener);

        verify(listener, times(5)).messageStarted(anyString(), anyInt(), eq(5));
        verify(listener, times(5)).messageFinished(any(WebDavMessage.class), anyInt(), eq(5));
    }

    @Test
    public void getMessages_shouldProvideCorrectHeadersInRequest() throws MessagingException {
        int totalMessages = 23;
        int messageStart = 1;
        int messageEnd = 11;
        setupFolderWithMessages(totalMessages);
        String messagesXml = "<xml>MessagesXml</xml>";
        buildSearchResponse(mockDataSet);
        when(mockStore.getMessagesXml()).thenReturn(messagesXml);
        when(mockStore.processRequest(eq("https://localhost/webDavStoreUrl/testFolder"), eq("SEARCH"),
                eq(messagesXml), Matchers.<Map<String, String>>any())).thenReturn(mockDataSet);

        folder.getMessages(messageStart, messageEnd, new Date(), listener);

        verify(mockStore, times(2)).processRequest(anyString(), anyString(), anyString(),
                headerCaptor.capture());
        assertEquals(2, headerCaptor.getValue().size());
        assertEquals("t", headerCaptor.getValue().get("Brief"));
        assertEquals("rows=" + (totalMessages - (messageEnd)) + "-" + (totalMessages - messageStart)
                , headerCaptor.getValue().get("Range"));
    }

    private void buildSearchResponse(DataSet mockDataSet) {
        String[] uids = new String[]{"uid1", "uid2", "uid3", "uid4", "uid5"};
        HashMap<String, String> uidToUrls = new HashMap<>();
        uidToUrls.put("uid1", "url1");
        uidToUrls.put("uid2", "url2");
        uidToUrls.put("uid3", "url3");
        uidToUrls.put("uid4", "url4");
        uidToUrls.put("uid5", "url5");

        when(mockDataSet.getUids()).thenReturn(uids);
        when(mockDataSet.getUidToUrl()).thenReturn(uidToUrls);
    }

    @Test(expected = MessagingException.class)
    public void getMessages_should_throw_message_exception_if_requesting_messages_from_empty_folder()
            throws MessagingException {
        folder.getMessages(0, 10, new Date(), listener);
    }

    private void setupMoveOrCopy() throws MessagingException {
        destinationFolder = setupDestinationFolder();
        String uid = "uid1";
        String url = "url1";
        messages = singletonList(createWebDavMessage(uid));
        setupGetUrlsRequestResponse(uid, url);
        when(mockStore.getMoveOrCopyMessagesReadXml(eq(new String[]{url}), anyBoolean())).thenReturn(moveOrCopyXml);
        moveOrCopyHeaders = new HashMap<>();
        moveOrCopyHeaders.put("Destination", "https://localhost/webDavStoreUrl/destFolder");
        moveOrCopyHeaders.put("Brief", "t");
        moveOrCopyHeaders.put("If-Match", "*");
    }

    @Test
    public void moveMessages_should_requestMoveXml() throws Exception {
        setupMoveOrCopy();

        folder.moveMessages(messages, destinationFolder);

        verify(mockStore).getMoveOrCopyMessagesReadXml(eq(new String[]{"url1"}),
                eq(true));
    }

    @Test
    public void moveMessages_should_send_move_command() throws Exception {
        setupMoveOrCopy();

        folder.moveMessages(messages, destinationFolder);

        verify(mockStore).processRequest("https://localhost/webDavStoreUrl/testFolder", "BMOVE",
                moveOrCopyXml, moveOrCopyHeaders, false);
    }

    @Test
    public void copyMessages_should_requestCopyXml() throws Exception {
        setupMoveOrCopy();

        folder.copyMessages(messages, destinationFolder);

        verify(mockStore).getMoveOrCopyMessagesReadXml(eq(new String[]{"url1"}),
                eq(false));
    }

    @Test
    public void copyMessages_should_send_copy_command() throws Exception {
        setupMoveOrCopy();

        folder.copyMessages(messages, destinationFolder);

        verify(mockStore).processRequest("https://localhost/webDavStoreUrl/testFolder", "BCOPY",
                moveOrCopyXml, moveOrCopyHeaders, false);
    }

    @Test
    public void appendWebDavMessages_replaces_messages_with_WebDAV_versions() throws MessagingException, IOException {
        List<Message> existingMessages = new ArrayList<>();
        Message existingMessage = mock(Message.class);
        existingMessages.add(existingMessage);
        String messageUid = "testMessageUid";
        when(existingMessage.getUid()).thenReturn(messageUid);

        List<? extends Message> response = folder.appendWebDavMessages(existingMessages);

        assertEquals(1, response.size(), 1);
        assertEquals(WebDavMessage.class, response.get(0).getClass());
        assertEquals(messageUid, response.get(0).getUid());
    }

    @Test
    public void appendWebDavMessages_sendsRequestUsingStore() throws MessagingException, IOException {
        List<Message> existingMessages = new ArrayList<>();
        Message existingMessage = mock(Message.class);
        existingMessages.add(existingMessage);
        String messageUid = "testMessageUid";
        when(existingMessage.getUid()).thenReturn(messageUid);

        folder.appendWebDavMessages(existingMessages);

        verify(mockStore).sendRequest(urlCaptor.capture(), eq("PUT"), entityCaptor.capture(),
                Matchers.<Map<String, String>>eq(null), eq(true));
        assertTrue(urlCaptor.getValue().startsWith(storeUrl + "/" + folderName + "/" + messageUid));
        assertTrue(urlCaptor.getValue().endsWith(".eml"));
    }
}