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


import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;

import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.DefaultBodyFactory;
import com.fsck.k9.mail.FetchProfile;
import com.fsck.k9.mail.FetchProfile.Item;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Folder;
import com.fsck.k9.mail.Folder.FolderType;
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.Part;
import com.fsck.k9.mail.internet.BinaryTempFileBody;
import com.fsck.k9.mail.internet.MimeHeader;
import com.fsck.k9.mail.store.StoreConfig;
import okio.Buffer;
import org.apache.james.mime4j.util.MimeUtil;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.RuntimeEnvironment;

import static com.fsck.k9.mail.Folder.OPEN_MODE_RO;
import static com.fsck.k9.mail.Folder.OPEN_MODE_RW;
import static com.fsck.k9.mail.store.imap.ImapResponseHelper.createImapResponse;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
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.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.mockito.internal.util.collections.Sets.newSet;


@RunWith(K9LibRobolectricTestRunner.class)
public class ImapFolderTest {
    private ImapStore imapStore;
    private ImapConnection imapConnection;
    private StoreConfig storeConfig;

    @Before
    public void setUp() throws Exception {
        BinaryTempFileBody.setTempDirectory(RuntimeEnvironment.application.getCacheDir());
        imapStore = mock(ImapStore.class);
        storeConfig = mock(StoreConfig.class);
        when(storeConfig.getInboxFolderName()).thenReturn("INBOX");
        when(imapStore.getCombinedPrefix()).thenReturn("");
        when(imapStore.getStoreConfig()).thenReturn(storeConfig);

        imapConnection = mock(ImapConnection.class);
    }

    @Test
    public void open_readWrite_shouldOpenFolder() throws Exception {
        ImapFolder imapFolder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);

        imapFolder.open(OPEN_MODE_RW);

        assertTrue(imapFolder.isOpen());
    }

    @Test
    public void open_readOnly_shouldOpenFolder() throws Exception {
        ImapFolder imapFolder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RO);

        imapFolder.open(OPEN_MODE_RO);

        assertTrue(imapFolder.isOpen());
    }

    @Test
    public void open_shouldFetchMessageCount() throws Exception {
        ImapFolder imapFolder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);

        imapFolder.open(OPEN_MODE_RW);

        assertEquals(23, imapFolder.getMessageCount());
    }

    @Test
    public void open_readWrite_shouldMakeGetModeReturnReadWrite() throws Exception {
        ImapFolder imapFolder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);

        imapFolder.open(OPEN_MODE_RW);

        assertEquals(OPEN_MODE_RW, imapFolder.getMode());
    }

    @Test
    public void open_readOnly_shouldMakeGetModeReturnReadOnly() throws Exception {
        ImapFolder imapFolder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RO);

        imapFolder.open(OPEN_MODE_RO);

        assertEquals(OPEN_MODE_RO, imapFolder.getMode());
    }

    @Test
    public void open_shouldMakeExistReturnTrueWithoutExecutingAdditionalCommands() throws Exception {
        ImapFolder imapFolder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);

        imapFolder.open(OPEN_MODE_RW);

        assertTrue(imapFolder.exists());
        verify(imapConnection, times(1)).executeSimpleCommand(anyString());
    }

    @Test
    public void open_calledTwice_shouldReuseSameImapConnection() throws Exception {
        ImapFolder imapFolder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        imapFolder.open(OPEN_MODE_RW);

        imapFolder.open(OPEN_MODE_RW);

        verify(imapStore, times(1)).getConnection();
    }

    @Test
    public void open_withConnectionThrowingOnReUse_shouldCreateNewImapConnection() throws Exception {
        ImapFolder imapFolder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        imapFolder.open(OPEN_MODE_RW);

        doThrow(IOException.class).when(imapConnection).executeSimpleCommand(Commands.NOOP);
        imapFolder.open(OPEN_MODE_RW);

        verify(imapStore, times(2)).getConnection();
    }

    @Test
    public void open_withIoException_shouldThrowMessagingException() throws Exception {
        ImapFolder imapFolder = createFolder("Folder");
        when(imapStore.getConnection()).thenReturn(imapConnection);
        doThrow(IOException.class).when(imapConnection).executeSimpleCommand("SELECT \"Folder\"");

        try {
            imapFolder.open(OPEN_MODE_RW);
            fail("Expected exception");
        } catch (MessagingException e) {
            assertNotNull(e.getCause());
            assertEquals(IOException.class, e.getCause().getClass());
        }
    }

    @Test
    public void open_withMessagingException_shouldThrowMessagingException() throws Exception {
        ImapFolder imapFolder = createFolder("Folder");
        when(imapStore.getConnection()).thenReturn(imapConnection);
        doThrow(MessagingException.class).when(imapConnection).executeSimpleCommand("SELECT \"Folder\"");

        try {
            imapFolder.open(OPEN_MODE_RW);
            fail("Expected exception");
        } catch (MessagingException ignored) {
        }
    }

    @Test
    public void open_withoutExistsResponse_shouldThrowMessagingException() throws Exception {
        ImapFolder imapFolder = createFolder("Folder");
        when(imapStore.getConnection()).thenReturn(imapConnection);
        List<ImapResponse> selectResponses = asList(
                createImapResponse("* OK [UIDNEXT 57576] Predicted next UID"),
                createImapResponse("2 OK [READ-WRITE] Select completed.")
        );
        when(imapConnection.executeSimpleCommand("SELECT \"Folder\"")).thenReturn(selectResponses);

        try {
            imapFolder.open(OPEN_MODE_RW);
            fail("Expected exception");
        } catch (MessagingException e) {
            assertEquals("Did not find message count during open", e.getMessage());
        }
    }

    @Test
    public void close_shouldCloseImapFolder() throws Exception {
        ImapFolder imapFolder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        imapFolder.open(OPEN_MODE_RW);

        imapFolder.close();

        assertFalse(imapFolder.isOpen());
    }

    @Test
    public void exists_withClosedFolder_shouldOpenConnectionAndIssueStatusCommand() throws Exception {
        ImapFolder imapFolder = createFolder("Folder");
        when(imapStore.getConnection()).thenReturn(imapConnection);

        imapFolder.exists();

        verify(imapConnection).executeSimpleCommand("STATUS \"Folder\" (UIDVALIDITY)");
    }

    @Test
    public void exists_withoutNegativeImapResponse_shouldReturnTrue() throws Exception {
        ImapFolder imapFolder = createFolder("Folder");
        when(imapStore.getConnection()).thenReturn(imapConnection);

        boolean folderExists = imapFolder.exists();

        assertTrue(folderExists);
    }

    @Test
    public void exists_withNegativeImapResponse_shouldReturnFalse() throws Exception {
        ImapFolder imapFolder = createFolder("Folder");
        when(imapStore.getConnection()).thenReturn(imapConnection);
        doThrow(NegativeImapResponseException.class).when(imapConnection)
                .executeSimpleCommand("STATUS \"Folder\" (UIDVALIDITY)");

        boolean folderExists = imapFolder.exists();

        assertFalse(folderExists);
    }

    @Test
    public void create_withClosedFolder_shouldOpenConnectionAndIssueCreateCommand() throws Exception {
        ImapFolder imapFolder = createFolder("Folder");
        when(imapStore.getConnection()).thenReturn(imapConnection);

        imapFolder.create(FolderType.HOLDS_MESSAGES);

        verify(imapConnection).executeSimpleCommand("CREATE \"Folder\"");
    }

    @Test
    public void create_withoutNegativeImapResponse_shouldReturnTrue() throws Exception {
        ImapFolder imapFolder = createFolder("Folder");
        when(imapStore.getConnection()).thenReturn(imapConnection);

        boolean success = imapFolder.create(FolderType.HOLDS_MESSAGES);

        assertTrue(success);
    }

    @Test
    public void create_withNegativeImapResponse_shouldReturnFalse() throws Exception {
        ImapFolder imapFolder = createFolder("Folder");
        when(imapStore.getConnection()).thenReturn(imapConnection);
        doThrow(NegativeImapResponseException.class).when(imapConnection).executeSimpleCommand("CREATE \"Folder\"");

        boolean success = imapFolder.create(FolderType.HOLDS_MESSAGES);

        assertFalse(success);
    }

    @Test
    public void copyMessages_withoutDestinationFolderOfWrongType_shouldThrow() throws Exception {
        ImapFolder sourceFolder = createFolder("Source");
        Folder destinationFolder = mock(Folder.class);
        List<ImapMessage> messages = singletonList(mock(ImapMessage.class));

        try {
            sourceFolder.copyMessages(messages, destinationFolder);
            fail("Expected exception");
        } catch (MessagingException e) {
            assertEquals("ImapFolder.copyMessages passed non-ImapFolder", e.getMessage());
        }
    }

    @Test
    public void copyMessages_withEmptyMessageList_shouldReturnNull() throws Exception {
        ImapFolder sourceFolder = createFolder("Source");
        ImapFolder destinationFolder = createFolder("Destination");
        List<ImapMessage> messages = Collections.emptyList();

        Map<String, String> uidMapping = sourceFolder.copyMessages(messages, destinationFolder);

        assertNull(uidMapping);
    }

    @Test
    public void copyMessages_withClosedFolder_shouldThrow() throws Exception {
        ImapFolder sourceFolder = createFolder("Source");
        ImapFolder destinationFolder = createFolder("Destination");
        when(imapStore.getConnection()).thenReturn(imapConnection);
        when(imapStore.getCombinedPrefix()).thenReturn("");
        List<ImapMessage> messages = singletonList(mock(ImapMessage.class));

        try {
            sourceFolder.copyMessages(messages, destinationFolder);
            fail("Expected exception");
        } catch (MessagingException e) {
            assertEquals("Folder Source is not open.", e.getMessage());
        }
    }

    @Test
    public void copyMessages() throws Exception {
        ImapFolder sourceFolder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        ImapFolder destinationFolder = createFolder("Destination");
        List<ImapMessage> messages = singletonList(createImapMessage("1"));
        List<ImapResponse> copyResponses = singletonList(
                createImapResponse("x OK [COPYUID 23 1 101] Success")
        );
        when(imapConnection.executeSimpleCommand("UID COPY 1 \"Destination\"")).thenReturn(copyResponses);
        sourceFolder.open(OPEN_MODE_RW);

        Map<String, String> uidMapping = sourceFolder.copyMessages(messages, destinationFolder);

        assertNotNull(uidMapping);
        assertEquals("101", uidMapping.get("1"));
    }

    @Test
    public void moveMessages_shouldCopyMessages() throws Exception {
        ImapFolder sourceFolder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        ImapFolder destinationFolder = createFolder("Destination");
        List<ImapMessage> messages = singletonList(createImapMessage("1"));
        List<ImapResponse> copyResponses = singletonList(
                createImapResponse("x OK [COPYUID 23 1 101] Success")
        );
        when(imapConnection.executeSimpleCommand("UID COPY 1 \"Destination\"")).thenReturn(copyResponses);
        sourceFolder.open(OPEN_MODE_RW);

        Map<String, String> uidMapping = sourceFolder.moveMessages(messages, destinationFolder);

        assertNotNull(uidMapping);
        assertEquals("101", uidMapping.get("1"));
    }

    @Test
    public void moveMessages_shouldDeleteMessagesFromSourceFolder() throws Exception {
        ImapFolder sourceFolder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        ImapFolder destinationFolder = createFolder("Destination");
        List<ImapMessage> messages = singletonList(createImapMessage("1"));
        List<ImapResponse> copyResponses = singletonList(
                createImapResponse("x OK [COPYUID 23 1 101] Success")
        );
        when(imapConnection.executeSimpleCommand("UID COPY 1 \"Destination\"")).thenReturn(copyResponses);
        sourceFolder.open(OPEN_MODE_RW);

        sourceFolder.moveMessages(messages, destinationFolder);

        verify(imapConnection).executeSimpleCommand("UID STORE 1 +FLAGS.SILENT (\\Deleted)");
    }

    @Test
    public void moveMessages_withEmptyMessageList_shouldReturnNull() throws Exception {
        ImapFolder sourceFolder = createFolder("Source");
        ImapFolder destinationFolder = createFolder("Destination");
        List<ImapMessage> messages = Collections.emptyList();

        Map<String, String> uidMapping = sourceFolder.moveMessages(messages, destinationFolder);

        assertNull(uidMapping);
    }

    @Test
    public void delete_withEmptyMessageList_shouldNotInteractWithImapConnection() throws Exception {
        ImapFolder folder = createFolder("Source");
        List<ImapMessage> messages = Collections.emptyList();

        folder.delete(messages, "Trash");

        verifyNoMoreInteractions(imapConnection);
    }

    @Test
    public void delete_fromTrashFolder_shouldIssueUidStoreFlagsCommand() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        List<ImapMessage> messages = singletonList(createImapMessage("23"));
        folder.open(OPEN_MODE_RW);

        folder.delete(messages, "Folder");

        verify(imapConnection).executeSimpleCommand("UID STORE 23 +FLAGS.SILENT (\\Deleted)");
    }

    @Test
    public void delete_shouldMoveMessagesToTrashFolder() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        ImapFolder trashFolder = createFolder("Trash");
        when(imapStore.getFolder("Trash")).thenReturn(trashFolder);
        List<ImapMessage> messages = singletonList(createImapMessage("2"));
        List<ImapResponse> copyResponses = singletonList(
                createImapResponse("x OK [COPYUID 23 2 102] Success")
        );
        when(imapConnection.executeSimpleCommand("UID COPY 2 \"Trash\"")).thenReturn(copyResponses);
        folder.open(OPEN_MODE_RW);

        folder.delete(messages, "Trash");

        verify(imapConnection).executeSimpleCommand("UID STORE 2 +FLAGS.SILENT (\\Deleted)");
    }

    @Test
    public void delete_withoutTrashFolderExisting_shouldCreateTrashFolder() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        ImapFolder trashFolder = createFolder("Trash");
        when(imapStore.getFolder("Trash")).thenReturn(trashFolder);
        List<ImapMessage> messages = singletonList(createImapMessage("2"));
        List<ImapResponse> copyResponses = singletonList(
                createImapResponse("x OK [COPYUID 23 2 102] Success")
        );
        when(imapConnection.executeSimpleCommand("UID COPY 2 \"Trash\"")).thenReturn(copyResponses);
        folder.open(OPEN_MODE_RW);
        doThrow(NegativeImapResponseException.class).doReturn(Collections.emptyList())
                .when(imapConnection).executeSimpleCommand("STATUS \"Trash\" (RECENT)");

        folder.delete(messages, "Trash");

        verify(imapConnection).executeSimpleCommand("CREATE \"Trash\"");
    }

    @Test
    public void getUnreadMessageCount_withClosedFolder_shouldThrow() throws Exception {
        ImapFolder folder = createFolder("Folder");
        when(imapStore.getConnection()).thenReturn(imapConnection);

        try {
            folder.getUnreadMessageCount();
            fail("Expected exception");
        } catch (MessagingException e) {
            assertCheckOpenErrorMessage("Folder", e);
        }
    }

    @Test
    public void getUnreadMessageCount_connectionThrowsIOException_shouldThrowMessagingException() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        when(imapConnection.executeSimpleCommand("SEARCH 1:* UNSEEN NOT DELETED")).thenThrow(new IOException());
        folder.open(OPEN_MODE_RW);

        try {
            folder.getUnreadMessageCount();
            fail("Expected exception");
        } catch (MessagingException e) {
            assertEquals("IO Error", e.getMessage());
        }
    }

    @Test
    public void getUnreadMessageCount() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        List<ImapResponse> imapResponses = singletonList(createImapResponse("* SEARCH 1 2 3"));
        when(imapConnection.executeSimpleCommand("SEARCH 1:* UNSEEN NOT DELETED")).thenReturn(imapResponses);
        folder.open(OPEN_MODE_RW);

        int unreadMessageCount = folder.getUnreadMessageCount();

        assertEquals(3, unreadMessageCount);
    }

    @Test
    public void getFlaggedMessageCount_withClosedFolder_shouldThrow() throws Exception {
        ImapFolder folder = createFolder("Folder");
        when(imapStore.getConnection()).thenReturn(imapConnection);

        try {
            folder.getFlaggedMessageCount();
            fail("Expected exception");
        } catch (MessagingException e) {
            assertCheckOpenErrorMessage("Folder", e);
        }
    }

    @Test
    public void getFlaggedMessageCount() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        List<ImapResponse> imapResponses = asList(
                createImapResponse("* SEARCH 1 2"),
                createImapResponse("* SEARCH 23 42")
        );
        when(imapConnection.executeSimpleCommand("SEARCH 1:* FLAGGED NOT DELETED")).thenReturn(imapResponses);
        folder.open(OPEN_MODE_RW);

        int flaggedMessageCount = folder.getFlaggedMessageCount();

        assertEquals(4, flaggedMessageCount);
    }

    @Test
    public void getHighestUid() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        List<ImapResponse> imapResponses = singletonList(createImapResponse("* SEARCH 42"));
        when(imapConnection.executeSimpleCommand("UID SEARCH *:*")).thenReturn(imapResponses);
        folder.open(OPEN_MODE_RW);

        long highestUid = folder.getHighestUid();

        assertEquals(42L, highestUid);
    }

    @Test
    public void getHighestUid_imapConnectionThrowsNegativesResponse_shouldReturnMinusOne() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        doThrow(NegativeImapResponseException.class).when(imapConnection).executeSimpleCommand("UID SEARCH *:*");
        folder.open(OPEN_MODE_RW);

        long highestUid = folder.getHighestUid();

        assertEquals(-1L, highestUid);
    }

    @Test
    public void getHighestUid_imapConnectionThrowsIOException_shouldThrowMessagingException() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        doThrow(IOException.class).when(imapConnection).executeSimpleCommand("UID SEARCH *:*");
        folder.open(OPEN_MODE_RW);

        try {
            folder.getHighestUid();
            fail("Expected MessagingException");
        } catch (MessagingException e) {
            assertEquals("IO Error", e.getMessage());
        }
    }

    @Test
    public void getMessages_withoutDateConstraint() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        List<ImapResponse> imapResponses = asList(
                createImapResponse("* SEARCH 3"),
                createImapResponse("* SEARCH 5"),
                createImapResponse("* SEARCH 6")
        );
        when(imapConnection.executeSimpleCommand("UID SEARCH 1:10 NOT DELETED")).thenReturn(imapResponses);
        folder.open(OPEN_MODE_RW);

        List<ImapMessage> messages = folder.getMessages(1, 10, null, null);

        assertNotNull(messages);
        assertEquals(newSet("3", "5", "6"), extractMessageUids(messages));
    }

    @Test
    public void getMessages_withDateConstraint() throws Exception {
        TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        List<ImapResponse> imapResponses = asList(
                createImapResponse("* SEARCH 47"),
                createImapResponse("* SEARCH 18")
        );
        when(imapConnection.executeSimpleCommand("UID SEARCH 1:10 SINCE 06-Feb-2016 NOT DELETED"))
                .thenReturn(imapResponses);
        folder.open(OPEN_MODE_RW);

        List<ImapMessage> messages = folder.getMessages(1, 10, new Date(1454719826000L), null);

        assertNotNull(messages);
        assertEquals(newSet("18", "47"), extractMessageUids(messages));
    }

    @Test
    public void getMessages_withListener_shouldCallListener() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        List<ImapResponse> imapResponses = singletonList(createImapResponse("* SEARCH 99"));
        when(imapConnection.executeSimpleCommand("UID SEARCH 1:10 NOT DELETED")).thenReturn(imapResponses);
        folder.open(OPEN_MODE_RW);
        MessageRetrievalListener<ImapMessage> listener = createMessageRetrievalListener();

        List<ImapMessage> messages = folder.getMessages(1, 10, null, listener);

        ImapMessage message = messages.get(0);
        verify(listener).messageStarted("99", 0, 1);
        verify(listener).messageFinished(message, 0, 1);
        verifyNoMoreInteractions(listener);
    }

    @Test
    public void getMessages_withInvalidStartArgument_shouldThrow() throws Exception {
        ImapFolder folder = createFolder("Folder");

        try {
            folder.getMessages(0, 10, null, null);
            fail("Expected exception");
        } catch (MessagingException e) {
            assertEquals("Invalid message set 0 10", e.getMessage());
        }
    }

    @Test
    public void getMessages_withInvalidEndArgument_shouldThrow() throws Exception {
        ImapFolder folder = createFolder("Folder");

        try {
            folder.getMessages(10, 0, null, null);
            fail("Expected exception");
        } catch (MessagingException e) {
            assertEquals("Invalid message set 10 0", e.getMessage());
        }
    }

    @Test
    public void getMessages_withEndArgumentSmallerThanStartArgument_shouldThrow() throws Exception {
        ImapFolder folder = createFolder("Folder");

        try {
            folder.getMessages(10, 5, null, null);
            fail("Expected exception");
        } catch (MessagingException e) {
            assertEquals("Invalid message set 10 5", e.getMessage());
        }
    }

    @Test
    public void getMessages_withClosedFolder_shouldThrow() throws Exception {
        ImapFolder folder = createFolder("Folder");
        when(imapStore.getConnection()).thenReturn(imapConnection);

        try {
            folder.getMessages(1, 5, null, null);
            fail("Expected exception");
        } catch (MessagingException e) {
            assertCheckOpenErrorMessage("Folder", e);
        }
    }

    @Test
    public void getMessages_sequenceNumbers_withClosedFolder_shouldThrow() throws Exception {
        ImapFolder folder = createFolder("Folder");
        when(imapStore.getConnection()).thenReturn(imapConnection);

        try {
            folder.getMessages(asList(1L, 2L, 5L), false, null);
            fail("Expected exception");
        } catch (MessagingException e) {
            assertCheckOpenErrorMessage("Folder", e);
        }
    }

    @Test
    public void getMessages_sequenceNumbers() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        List<ImapResponse> imapResponses = asList(
                createImapResponse("* SEARCH 17"),
                createImapResponse("* SEARCH 18"),
                createImapResponse("* SEARCH 49")
        );
        when(imapConnection.executeSimpleCommand("UID SEARCH 1,2,5 NOT DELETED")).thenReturn(imapResponses);
        folder.open(OPEN_MODE_RW);

        List<ImapMessage> messages = folder.getMessages(asList(1L, 2L, 5L), false, null);

        assertNotNull(messages);
        assertEquals(newSet("17", "18", "49"), extractMessageUids(messages));
    }

    @Test
    public void getMessages_sequenceNumbers_withListener_shouldCallListener() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        List<ImapResponse> imapResponses = singletonList(createImapResponse("* SEARCH 99"));
        when(imapConnection.executeSimpleCommand("UID SEARCH 1")).thenReturn(imapResponses);
        folder.open(OPEN_MODE_RW);
        MessageRetrievalListener<ImapMessage> listener = createMessageRetrievalListener();

        List<ImapMessage> messages = folder.getMessages(singletonList(1L), true, listener);

        ImapMessage message = messages.get(0);
        verify(listener).messageStarted("99", 0, 1);
        verify(listener).messageFinished(message, 0, 1);
        verifyNoMoreInteractions(listener);
    }

    @Test
    public void getMessagesFromUids_withClosedFolder_shouldThrow() throws Exception {
        ImapFolder folder = createFolder("Folder");
        when(imapStore.getConnection()).thenReturn(imapConnection);

        try {
            folder.getMessagesFromUids(asList("11", "22", "25"));
            fail("Expected exception");
        } catch (MessagingException e) {
            assertCheckOpenErrorMessage("Folder", e);
        }
    }

    @Test
    public void getMessagesFromUids() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        List<ImapResponse> imapResponses = asList(
                createImapResponse("* SEARCH 11"),
                createImapResponse("* SEARCH 22"),
                createImapResponse("* SEARCH 25")
        );
        when(imapConnection.executeSimpleCommand("UID SEARCH UID 11,22,25")).thenReturn(imapResponses);
        folder.open(OPEN_MODE_RW);

        List<ImapMessage> messages = folder.getMessagesFromUids(asList("11", "22", "25"));

        assertNotNull(messages);
        assertEquals(newSet("11", "22", "25"), extractMessageUids(messages));
    }

    @Test
    public void areMoreMessagesAvailable_withClosedFolder_shouldThrow() throws Exception {
        ImapFolder folder = createFolder("Folder");
        when(imapStore.getConnection()).thenReturn(imapConnection);

        try {
            folder.areMoreMessagesAvailable(10, new Date());
            fail("Expected exception");
        } catch (MessagingException e) {
            assertCheckOpenErrorMessage("Folder", e);
        }
    }

    @Test
    public void areMoreMessagesAvailable_withAdditionalMessages_shouldReturnTrue() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        List<ImapResponse> imapResponses = singletonList(createImapResponse("* SEARCH 42"));
        when(imapConnection.executeSimpleCommand("SEARCH 1:9 NOT DELETED")).thenReturn(imapResponses);
        folder.open(OPEN_MODE_RW);

        boolean areMoreMessagesAvailable = folder.areMoreMessagesAvailable(10, null);

        assertTrue(areMoreMessagesAvailable);
    }

    @Test
    public void areMoreMessagesAvailable_withoutAdditionalMessages_shouldReturnFalse() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        folder.open(OPEN_MODE_RW);

        boolean areMoreMessagesAvailable = folder.areMoreMessagesAvailable(600, null);

        assertFalse(areMoreMessagesAvailable);
    }

    @Test
    public void areMoreMessagesAvailable_withIndexOfOne_shouldReturnFalseWithoutPerformingSearch() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        folder.open(OPEN_MODE_RW);

        boolean areMoreMessagesAvailable = folder.areMoreMessagesAvailable(1, null);

        assertFalse(areMoreMessagesAvailable);
        //SELECT during OPEN and no more
        verify(imapConnection, times(1)).executeSimpleCommand(anyString());
    }

    @Test
    public void areMoreMessagesAvailable_withoutAdditionalMessages_shouldIssueSearchCommandsUntilAllMessagesSearched()
            throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        folder.open(OPEN_MODE_RW);

        folder.areMoreMessagesAvailable(600, null);

        verify(imapConnection).executeSimpleCommand("SEARCH 100:599 NOT DELETED");
        verify(imapConnection).executeSimpleCommand("SEARCH 1:99 NOT DELETED");
    }

    @Test
    public void fetch_withNullMessageListArgument_shouldDoNothing() throws Exception {
        ImapFolder folder = createFolder("Folder");
        FetchProfile fetchProfile = createFetchProfile();

        folder.fetch(null, fetchProfile, null);

        verifyNoMoreInteractions(imapStore);
    }

    @Test
    public void fetch_withEmptyMessageListArgument_shouldDoNothing() throws Exception {
        ImapFolder folder = createFolder("Folder");
        FetchProfile fetchProfile = createFetchProfile();

        folder.fetch(Collections.<ImapMessage>emptyList(), fetchProfile, null);

        verifyNoMoreInteractions(imapStore);
    }

    @Test
    public void fetch_withFlagsFetchProfile_shouldIssueRespectiveCommand() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RO);
        folder.open(OPEN_MODE_RO);
        when(imapConnection.readResponse(any(ImapResponseCallback.class))).thenReturn(createImapResponse("x OK"));
        List<ImapMessage> messages = createImapMessages("1");
        FetchProfile fetchProfile = createFetchProfile(Item.FLAGS);

        folder.fetch(messages, fetchProfile, null);

        verify(imapConnection).sendCommand("UID FETCH 1 (UID FLAGS)", false);
    }

    @Test
    public void fetch_withEnvelopeFetchProfile_shouldIssueRespectiveCommand() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RO);
        folder.open(OPEN_MODE_RO);
        when(imapConnection.readResponse(any(ImapResponseCallback.class))).thenReturn(createImapResponse("x OK"));
        List<ImapMessage> messages = createImapMessages("1");
        FetchProfile fetchProfile = createFetchProfile(Item.ENVELOPE);

        folder.fetch(messages, fetchProfile, null);

        verify(imapConnection).sendCommand("UID FETCH 1 (UID INTERNALDATE RFC822.SIZE BODY.PEEK[HEADER.FIELDS " +
                "(date subject from content-type to cc reply-to message-id references in-reply-to X-K9mail-Identity)]" +
                ")", false);
    }

    @Test
    public void fetch_withStructureFetchProfile_shouldIssueRespectiveCommand() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RO);
        folder.open(OPEN_MODE_RO);
        when(imapConnection.readResponse(any(ImapResponseCallback.class))).thenReturn(createImapResponse("x OK"));
        List<ImapMessage> messages = createImapMessages("1");
        FetchProfile fetchProfile = createFetchProfile(Item.STRUCTURE);

        folder.fetch(messages, fetchProfile, null);

        verify(imapConnection).sendCommand("UID FETCH 1 (UID BODYSTRUCTURE)", false);
    }

    @Test
    public void fetch_withBodySaneFetchProfile_shouldIssueRespectiveCommand() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RO);
        folder.open(OPEN_MODE_RO);
        when(imapConnection.readResponse(any(ImapResponseCallback.class))).thenReturn(createImapResponse("x OK"));
        List<ImapMessage> messages = createImapMessages("1");
        FetchProfile fetchProfile = createFetchProfile(Item.BODY_SANE);
        when(storeConfig.getMaximumAutoDownloadMessageSize()).thenReturn(4096);

        folder.fetch(messages, fetchProfile, null);

        verify(imapConnection).sendCommand("UID FETCH 1 (UID BODY.PEEK[]<0.4096>)", false);
    }

    @Test
    public void fetch_withBodySaneFetchProfileAndNoMaximumDownloadSize_shouldIssueRespectiveCommand() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RO);
        folder.open(OPEN_MODE_RO);
        when(imapConnection.readResponse(any(ImapResponseCallback.class))).thenReturn(createImapResponse("x OK"));
        List<ImapMessage> messages = createImapMessages("1");
        FetchProfile fetchProfile = createFetchProfile(Item.BODY_SANE);
        when(storeConfig.getMaximumAutoDownloadMessageSize()).thenReturn(0);

        folder.fetch(messages, fetchProfile, null);

        verify(imapConnection).sendCommand("UID FETCH 1 (UID BODY.PEEK[])", false);
    }

    @Test
    public void fetch_withBodyFetchProfileAndNoMaximumDownloadSize_shouldIssueRespectiveCommand() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RO);
        folder.open(OPEN_MODE_RO);
        when(imapConnection.readResponse(any(ImapResponseCallback.class))).thenReturn(createImapResponse("x OK"));
        List<ImapMessage> messages = createImapMessages("1");
        FetchProfile fetchProfile = createFetchProfile(Item.BODY);

        folder.fetch(messages, fetchProfile, null);

        verify(imapConnection).sendCommand("UID FETCH 1 (UID BODY.PEEK[])", false);
    }

    @Test
    public void fetch_withFlagsFetchProfile_shouldSetFlags() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RO);
        folder.open(OPEN_MODE_RO);
        List<ImapMessage> messages = createImapMessages("1");
        FetchProfile fetchProfile = createFetchProfile(Item.FLAGS);
        when(imapConnection.readResponse(any(ImapResponseCallback.class)))
                .thenReturn(createImapResponse("* 1 FETCH (FLAGS (\\Seen) UID 1)"))
                .thenReturn(createImapResponse("x OK"));

        folder.fetch(messages, fetchProfile, null);

        ImapMessage imapMessage = messages.get(0);
        verify(imapMessage).setFlagInternal(Flag.SEEN, true);
    }

    @Test
    public void fetchPart_withTextSection_shouldIssueRespectiveCommand() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RO);
        when(storeConfig.getMaximumAutoDownloadMessageSize()).thenReturn(4096);
        folder.open(OPEN_MODE_RO);
        ImapMessage message = createImapMessage("1");
        Part part = createPart("TEXT");
        when(imapConnection.readResponse(any(ImapResponseCallback.class))).thenReturn(createImapResponse("x OK"));

        folder.fetchPart(message, part, null, null);

        verify(imapConnection).sendCommand("UID FETCH 1 (UID BODY.PEEK[TEXT]<0.4096>)", false);
    }

    @Test
    public void fetchPart_withNonTextSection_shouldIssueRespectiveCommand() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RO);
        folder.open(OPEN_MODE_RO);
        ImapMessage message = createImapMessage("1");
        Part part = createPart("1.1");
        when(imapConnection.readResponse(any(ImapResponseCallback.class))).thenReturn(createImapResponse("x OK"));

        folder.fetchPart(message, part, null, null);

        verify(imapConnection).sendCommand("UID FETCH 1 (UID BODY.PEEK[1.1])", false);
    }

    @Test
    public void fetchPart_withTextSection_shouldProcessImapResponses() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RO);
        folder.open(OPEN_MODE_RO);
        ImapMessage message = createImapMessage("1");
        Part part = createPlainTextPart("1.1");
        setupSingleFetchResponseToCallback();

        folder.fetchPart(message, part, null, new DefaultBodyFactory());

        ArgumentCaptor<Body> bodyArgumentCaptor = ArgumentCaptor.forClass(Body.class);
        verify(part).setBody(bodyArgumentCaptor.capture());
        Body body = bodyArgumentCaptor.getValue();
        Buffer buffer = new Buffer();
        body.writeTo(buffer.outputStream());
        assertEquals("text", buffer.readUtf8());
    }

    @Test
    public void appendMessages_shouldIssueRespectiveCommand() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        folder.open(OPEN_MODE_RW);
        List<ImapMessage> messages = createImapMessages("1");
        when(imapConnection.readResponse()).thenReturn(createImapResponse("x OK [APPENDUID 1 23]"));

        folder.appendMessages(messages);

        verify(imapConnection).sendCommand("APPEND \"Folder\" () {0}", false);
    }

    @Test
    public void getUidFromMessageId_withoutMessageIdHeader_shouldReturnNull() throws Exception {
        ImapFolder folder = createFolder("Folder");
        ImapMessage message = createImapMessage("2");
        when(message.getHeader("Message-ID")).thenReturn(new String[0]);

        String uid = folder.getUidFromMessageId(message);

        assertNull(uid);
    }

    @Test
    public void getUidFromMessageId_withMessageIdHeader_shouldIssueUidSearchCommand() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        folder.open(OPEN_MODE_RW);
        ImapMessage message = createImapMessage("2");
        when(message.getHeader("Message-ID")).thenReturn(new String[] { "<00000000.0000000@example.org>" });

        folder.getUidFromMessageId(message);

        verify(imapConnection).executeSimpleCommand("UID SEARCH HEADER MESSAGE-ID \"<00000000.0000000@example.org>\"");
    }

    @Test
    public void getUidFromMessageId() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        folder.open(OPEN_MODE_RW);
        ImapMessage message = createImapMessage("2");
        when(message.getHeader("Message-ID")).thenReturn(new String[] { "<00000000.0000000@example.org>" });
        when(imapConnection.executeSimpleCommand("UID SEARCH HEADER MESSAGE-ID \"<00000000.0000000@example.org>\""))
                .thenReturn(singletonList(createImapResponse("* SEARCH 23")));

        String uid = folder.getUidFromMessageId(message);

        assertEquals("23", uid);
    }

    @Test
    public void expunge_shouldIssueExpungeCommand() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);

        folder.expunge();

        verify(imapConnection).executeSimpleCommand("EXPUNGE");
    }

    @Test
    public void setFlags_shouldIssueUidStoreCommand() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);

        folder.setFlags(newSet(Flag.SEEN), true);

        verify(imapConnection).executeSimpleCommand("UID STORE 1:* +FLAGS.SILENT (\\Seen)");
    }

    @Test
    public void getNewPushState_withNewerUid_shouldReturnNewPushState() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        ImapMessage message = createImapMessage("2");

        String newPushState = folder.getNewPushState("uidNext=2", message);

        assertEquals("uidNext=3", newPushState);
    }

    @Test
    public void getNewPushState_withoutNewerUid_shouldReturnNull() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RW);
        ImapMessage message = createImapMessage("1");

        String newPushState = folder.getNewPushState("uidNext=2", message);

        assertNull(newPushState);
    }

    @Test
    public void search_withFullTextSearchEnabled_shouldIssueRespectiveCommand() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RO);
        when(storeConfig.allowRemoteSearch()).thenReturn(true);
        when(storeConfig.isRemoteSearchFullText()).thenReturn(true);

        folder.search("query", newSet(Flag.SEEN), Collections.<Flag>emptySet());

        verify(imapConnection).executeSimpleCommand("UID SEARCH SEEN TEXT \"query\"");
    }

    @Test
    public void search_withFullTextSearchDisabled_shouldIssueRespectiveCommand() throws Exception {
        ImapFolder folder = createFolder("Folder");
        prepareImapFolderForOpen(OPEN_MODE_RO);
        when(storeConfig.allowRemoteSearch()).thenReturn(true);
        when(storeConfig.isRemoteSearchFullText()).thenReturn(false);

        folder.search("query", Collections.<Flag>emptySet(), Collections.<Flag>emptySet());

        verify(imapConnection).executeSimpleCommand("UID SEARCH OR SUBJECT \"query\" FROM \"query\"");
    }

    @Test
    public void search_withRemoteSearchDisabled_shouldThrow() throws Exception {
        ImapFolder folder = createFolder("Folder");
        when(storeConfig.allowRemoteSearch()).thenReturn(false);

        try {
            folder.search("query", Collections.<Flag>emptySet(), Collections.<Flag>emptySet());
            fail("Expected exception");
        } catch (MessagingException e) {
            assertEquals("Your settings do not allow remote searching of this account", e.getMessage());
        }
    }

    @Test(expected = Error.class)
    public void delete_notImplemented() throws Exception {
        ImapFolder folder = createFolder("Folder");

        folder.delete(false);
    }

    @Test
    public void getMessageByUid_returnsNewImapMessageWithUidInFolder() throws Exception {
        ImapFolder folder = createFolder("Folder");

        ImapMessage message = folder.getMessage("uid");

        assertEquals("uid", message.getUid());
        assertEquals(folder, message.getFolder());
    }

    private Part createPlainTextPart(String serverExtra) {
        Part part = createPart(serverExtra);
        when(part.getHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)).thenReturn(
                new String[] { MimeUtil.ENC_7BIT }
        );
        when(part.getHeader(MimeHeader.HEADER_CONTENT_TYPE)).thenReturn(
                new String[] { "text/plain" }
        );
        return part;
    }

    private void setupSingleFetchResponseToCallback() throws IOException {
        when(imapConnection.readResponse(any(ImapResponseCallback.class)))
                .thenAnswer(new Answer<ImapResponse>() {
                    @Override
                    public ImapResponse answer(InvocationOnMock invocation) throws Throwable {
                        ImapResponseCallback callback = (ImapResponseCallback) invocation.getArguments()[0];
                        return buildImapFetchResponse(callback);
                    }
                })
                .thenAnswer(new Answer<ImapResponse>() {
                    @Override
                    public ImapResponse answer(InvocationOnMock invocation) throws Throwable {
                        ImapResponseCallback callback = (ImapResponseCallback) invocation.getArguments()[0];
                        return ImapResponse.newTaggedResponse(callback, "TAG");
                    }
                });
    }

    private ImapResponse buildImapFetchResponse(ImapResponseCallback callback) {
        ImapResponse response = ImapResponse.newContinuationRequest(callback);
        response.add("1");
        response.add("FETCH");
        ImapList fetchList = new ImapList();
        fetchList.add("UID");
        fetchList.add("1");
        fetchList.add("BODY");
        fetchList.add("1.1");
        fetchList.add("text");
        response.add(fetchList);
        return response;
    }

    private Set<String> extractMessageUids(List<ImapMessage> messages) {
        Set<String> result = new HashSet<>();
        for (Message message : messages) {
            result.add(message.getUid());
        }

        return result;
    }

    private ImapFolder createFolder(String folderName) {
        return new ImapFolder(imapStore, folderName, FolderNameCodec.newInstance());
    }

    private ImapMessage createImapMessage(String uid) {
        ImapMessage message = mock(ImapMessage.class);
        when(message.getUid()).thenReturn(uid);

        return message;
    }

    private List<ImapMessage> createImapMessages(String... uids) {
        List<ImapMessage> imapMessages = new ArrayList<>(uids.length);

        for (String uid : uids) {
            ImapMessage imapMessage = createImapMessage(uid);
            imapMessages.add(imapMessage);
        }

        return imapMessages;
    }

    private Part createPart(String serverExtra) {
        Part part = mock(Part.class);
        when(part.getServerExtra()).thenReturn(serverExtra);

        return part;
    }

    private FetchProfile createFetchProfile(Item... items) {
        FetchProfile fetchProfile = new FetchProfile();
        Collections.addAll(fetchProfile, items);

        return fetchProfile;
    }

    @SuppressWarnings("unchecked")
    private MessageRetrievalListener<ImapMessage> createMessageRetrievalListener() {
        return mock(MessageRetrievalListener.class);
    }

    private void prepareImapFolderForOpen(int openMode) throws MessagingException, IOException {
        when(imapStore.getConnection()).thenReturn(imapConnection);
        List<ImapResponse> imapResponses = asList(
                createImapResponse("* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft NonJunk $MDNSent)"),
                createImapResponse("* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft NonJunk " +
                        "$MDNSent \\*)] Flags permitted."),
                createImapResponse("* 23 EXISTS"),
                createImapResponse("* 0 RECENT"),
                createImapResponse("* OK [UIDVALIDITY 1125022061] UIDs valid"),
                createImapResponse("* OK [UIDNEXT 57576] Predicted next UID"),
                (openMode == OPEN_MODE_RW) ?
                        createImapResponse("2 OK [READ-WRITE] Select completed.") :
                        createImapResponse("2 OK [READ-ONLY] Examine completed.")
        );

        if (openMode == OPEN_MODE_RW) {
            when(imapConnection.executeSimpleCommand("SELECT \"Folder\"")).thenReturn(imapResponses);
        } else {
            when(imapConnection.executeSimpleCommand("EXAMINE \"Folder\"")).thenReturn(imapResponses);
        }
    }

    private void assertCheckOpenErrorMessage(String folderName, MessagingException e) {
        assertEquals("Folder " + folderName + " is not open.", e.getMessage());
    }
}
