package com.github.drapostolos.rdp4j;

import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.Mockito.never;

import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import org.assertj.core.api.Assertions;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import com.github.drapostolos.rdp4j.spi.FileElement;
import com.github.drapostolos.rdp4j.spi.PolledDirectory;

public class AcceptanceTest extends EventVerifier {

    private DirectoryPoller dp;
    private DirectoryPollerBuilder builder;

    @Before
    public void testFixture() throws Exception {
        directoryMock = Mockito.mock(PolledDirectory.class);
        listenerMock = Mockito.mock(AbstractRdp4jListener.class);
        inOrder = Mockito.inOrder(listenerMock);
        builder = DirectoryPoller.newBuilder();
    }

    @After
    public void cleanup() throws Exception {
        if (dp != null && !dp.isTerminated()) {
            dp.stop();
        }
    }

    @Test(timeout = 1000)
    public void canInterruptBeforePollingCycle() throws Exception {

        // given 
        final StringBuffer checker = new StringBuffer();
        final CountDownLatch latch = new CountDownLatch(1);
        Mockito.when(directoryMock.listFiles())
                .thenReturn(list("file1.txt/1"));

        // when
        dp = builder
                .addPolledDirectory(directoryMock)
                .addListener(new AbstractRdp4jListener() {

                    @Override
                    public void beforePollingCycle(BeforePollingCycleEvent event) throws InterruptedException {
                        latch.countDown();
                        SECONDS.sleep(5);
                    }

                    @Override
                    public void afterPollingCycle(AfterPollingCycleEvent event) throws InterruptedException {
                        checker.append("X"); // this should not be called.
                    }

                    @Override
                    public void afterStop(AfterStopEvent event) {
                        checker.append("PASS");
                    }
                })
                .enableFileAddedEventsForInitialContent()
                .setPollingInterval(10, TimeUnit.MILLISECONDS)
                .start();

        // then
        latch.await();
        dp.stopNow();

        assertThat(checker.toString()).isEqualTo("PASS");
    }

    @Test(timeout = 1000)
    public void canInterruptAfterPollingCycle() throws Exception {

        // given 
        final StringBuffer checker = new StringBuffer();
        final CountDownLatch latch = new CountDownLatch(1);
        Mockito.when(directoryMock.listFiles())
                .thenReturn(list("file1.txt/1"));

        // when
        dp = builder
                .addPolledDirectory(directoryMock)
                .addListener(new AbstractRdp4jListener() {

                    @Override
                    public void afterPollingCycle(AfterPollingCycleEvent event) throws InterruptedException {
                        latch.countDown();
                        SECONDS.sleep(5);
                    }

                    @Override
                    public void afterStop(AfterStopEvent event) {
                        checker.append("PASS");
                    }
                })
                .enableFileAddedEventsForInitialContent()
                .setPollingInterval(10, TimeUnit.MILLISECONDS)
                .start();

        // then
        latch.await();
        dp.stopNow();

        assertThat(checker.toString()).isEqualTo("PASS");
    }

    @Test(timeout = 1000)
    public void canInterruptFileAddedEvent() throws Exception {

        // given 
        final StringBuffer checker = new StringBuffer();
        final CountDownLatch latch = new CountDownLatch(1);
        Mockito.when(directoryMock.listFiles())
                .thenReturn(list("file1.txt/1"));

        // when
        dp = builder
                .addPolledDirectory(directoryMock)
                .addListener(new AbstractRdp4jListener() {

                    @Override
                    public void fileAdded(FileAddedEvent event) throws InterruptedException {
                        latch.countDown();
                        SECONDS.sleep(5);
                    }

                    @Override
                    public void afterPollingCycle(AfterPollingCycleEvent event) throws InterruptedException {
                        checker.append("X"); // this should not be called.
                    }

                    @Override
                    public void afterStop(AfterStopEvent event) {
                        checker.append("PASS");
                    }
                })
                .enableFileAddedEventsForInitialContent()
                .setPollingInterval(10, TimeUnit.MILLISECONDS)
                .start();

        // then
        latch.await();
        dp.stopNow();

        assertThat(checker.toString()).isEqualTo("PASS");
    }

    @Test(timeout = 1000)
    public void canInterruptWhenProcessingMultipleListeners() throws Exception {

        // given 
        final CountDownLatch latch = new CountDownLatch(1);
        Mockito.when(directoryMock.listFiles())
                .thenReturn(list("file1.txt/1"));

        // when
        dp = builder
                .addPolledDirectory(directoryMock)
                .addListener(new AbstractRdp4jListener() {

                    @Override
                    public void beforePollingCycle(BeforePollingCycleEvent event) throws InterruptedException {
                        latch.countDown();
                        SECONDS.sleep(10);
                    }
                })
                .addListener(new AbstractRdp4jListener() {

                    @Override
                    public void beforePollingCycle(BeforePollingCycleEvent event) throws InterruptedException {
                        latch.countDown();
                        SECONDS.sleep(10);
                    }
                })
                .enableFileAddedEventsForInitialContent()
                .setPollingInterval(10, TimeUnit.MILLISECONDS)
                .start();

        // then
        latch.await();
        dp.stopNow();
    }

    @Test
    public void canStartAsynchronously() throws Exception {
        // given 
        final CountDownLatch latch = new CountDownLatch(1);

        // when
        DirectoryPollerFuture future = builder
                .addPolledDirectory(directoryMock)
                .addListener(new AbstractRdp4jListener() {

                    @Override
                    public void beforeStart(BeforeStartEvent event) {
                        try {
                            latch.await();
                        } catch (InterruptedException e) {
                            fail("should not be interrupted here!");
                        }
                    }
                })
                .setPollingInterval(10, TimeUnit.MILLISECONDS)
                .startAsync();

        assertThat(future.isStarted()).isFalse();
        latch.countDown();
        dp = future.get();
        assertThat(dp).isNotNull();
        assertThat(future.isStarted()).isTrue();

    }

    @Test(timeout = 2000)
    public void shouldStopDirectoryPollerWhenRuntimeExceptionIsThrownByImplementation() throws Exception {
        // given 
        Mockito.when(directoryMock.listFiles())
                .thenReturn(list())
                .thenThrow(new RuntimeException());

        // when
        dp = builder
                .addPolledDirectory(new PolledDirectory() {

                    @Override
                    public Set<FileElement> listFiles() throws IOException {
                        throw new RuntimeException();
                    }
                })
                .setPollingInterval(10, TimeUnit.MILLISECONDS)
                .start();

        // then should stop automatically.
        dp.awaitTermination();

    }

    @Test
    public void enableParallelDirectoryPolling() throws Exception {
        // given 
        Mockito.when(directoryMock.listFiles())
                .thenReturn(list());

        // when
        dp = builder
                .addPolledDirectory(directoryMock)
                .enableParallelPollingOfDirectories()
                .setPollingInterval(10, TimeUnit.MILLISECONDS)
                .start();

        // then
        Assertions.assertThat(dp.isParallelDirectoryPollingEnabled()).isTrue();
    }

    @Test
    public void defaultParallelDirectoryPolling() throws Exception {
        // given 
        Mockito.when(directoryMock.listFiles())
                .thenReturn(list());

        // when
        dp = builder
                .addPolledDirectory(directoryMock)
                .start();

        // then
        Assertions.assertThat(dp.isParallelDirectoryPollingEnabled()).isFalse();
    }

    @Test
    public void enableFileAddedEventsForInitialContent() throws Exception {
        // given 
        Mockito.when(directoryMock.listFiles())
                .thenReturn(list());

        // when
        dp = builder
                .addPolledDirectory(directoryMock)
                .enableFileAddedEventsForInitialContent()
                .setPollingInterval(10, TimeUnit.MILLISECONDS)
                .start();

        // then
        Assertions.assertThat(dp.isFileAdedEventForInitialContentEnabled()).isTrue();
    }

    @Test
    public void defaultFileAddedEventsForInitialContent() throws Exception {
        // given 
        Mockito.when(directoryMock.listFiles())
                .thenReturn(list());

        // when
        dp = builder
                .addPolledDirectory(directoryMock)
                .start();

        // then
        Assertions.assertThat(dp.isFileAdedEventForInitialContentEnabled()).isFalse();
    }

    @Test
    public void fileAddedEventEnabledForInitialContent() throws Exception {
        // given 
        Mockito.when(directoryMock.listFiles())
                .thenReturn(list("file1.txt/1"));

        // when
        dp = builder
                .addPolledDirectory(directoryMock)
                .addListener(new PollCycleCounter().stopPollingAfterNumOfCycles(2))
                .addListener(listenerMock)
                .enableFileAddedEventsForInitialContent()
                .setPollingInterval(10, TimeUnit.MILLISECONDS)
                .start();
        dp.awaitTermination();

        // then
        verifyEventsInOrder(
                BeforeStartEvent.class,
                BeforePollingCycleEvent.class,
                FileAddedEvent.class,
                InitialContentEvent.class,
                AfterPollingCycleEvent.class,
                BeforePollingCycleEvent.class,
                AfterPollingCycleEvent.class,
                AfterStopEvent.class);
    }

    @Test
    public void addRemoveDirectories() throws Exception {
        // given 
        PolledDirectory directoryMock2 = Mockito.mock(PolledDirectory.class);

        // when
        PollCycleCounter latch = new PollCycleCounter();
        dp = builder
                .addPolledDirectory(directoryMock)
                .addListener(latch)
                .setPollingInterval(5, TimeUnit.MILLISECONDS)
                .start();
        dp.addPolledDirectory(directoryMock2);

        // then 
        latch.awaitAtLeastNumPollCycles(1);
        Assertions.assertThat(dp.getPolledDirectories()).contains(directoryMock, directoryMock2);

        // when 
        dp.removePolledDirectory(directoryMock);
        dp.removePolledDirectory(directoryMock2);

        // then
        latch.awaitAtLeastNumPollCycles(1);
        Assertions.assertThat(dp.getPolledDirectories()).isEmpty();
    }

    // listenerCanReceiveFileAddedEventWhenListenerAddedAfterStart
    @Test
    public void listenerCanReceiveFileAddedEventWhenListenerAddedAfterStart() throws Exception {
        // given 
        Mockito.when(directoryMock.listFiles())
                .thenReturn(list())
                .thenReturn(list())
                .thenReturn(list("file.txt/12"));

        // when
        PollCycleCounter latch = new PollCycleCounter();
        dp = DirectoryPoller.newBuilder()
                .addListener(latch)
                .addPolledDirectory(directoryMock)
                .setPollingInterval(10, MILLISECONDS)
                .start();
        dp.addListener(listenerMock);
        latch.awaitAtLeastNumPollCycles(5);
        dp.stop();

        // then
        verifyEventsInOrder(
                BeforePollingCycleEvent.class,
                FileAddedEvent.class,
                AfterPollingCycleEvent.class);
    }

    @Test
    public void removeListener() throws Exception {
        // given 
        Mockito.when(directoryMock.listFiles())
                .thenReturn(list())
                .thenReturn(list("file.txt/12"));

        // when
        PollCycleCounter counter = new PollCycleCounter();
        dp = builder
                .addPolledDirectory(directoryMock)
                .addListener(counter)
                .addListener(listenerMock)
                .setPollingInterval(10, TimeUnit.MILLISECONDS)
                .start();
        counter.awaitAtLeastNumPollCycles(1);
        dp.removeListener(listenerMock);
        counter.awaitAtLeastNumPollCycles(1);

        verifyEventsInOrder(
                BeforePollingCycleEvent.class,
                AfterPollingCycleEvent.class);
        Mockito.verify(listenerMock, never()).afterStop(Mockito.any(AfterStopEvent.class));
    }

    @Test
    public void toStringValue() throws Exception {
        // given 
        Mockito.when(directoryMock.listFiles())
                .thenReturn(list());

        // when
        dp = builder
                .addPolledDirectory(directoryMock)
                .start();

        Assertions.assertThat(dp.toString()).matches("DirectoryPoller-\\d+: .*\\[polling every: 1000 milliseconds\\]");

    }

    @Test
    public void OneSuccesfulPoll() throws Exception {
        // given 
        Mockito.when(directoryMock.listFiles())
                .thenReturn(list());

        // when
        PollCycleCounter counter = new PollCycleCounter();
        dp = builder
                .addListener(listenerMock)
                .addListener(counter.stopPollingAfterNumOfCycles(1))
                .addPolledDirectory(directoryMock)
                .setPollingInterval(20, MILLISECONDS)
                .start();
        dp.awaitTermination();

        // then
        Assertions.assertThat(dp.getThreadName()).matches("DirectoryPoller-\\d+");
        verifyEventsInOrder(
                BeforeStartEvent.class,
                BeforePollingCycleEvent.class,
                InitialContentEvent.class,
                AfterPollingCycleEvent.class,
                AfterStopEvent.class);
    }

    @Test
    public void OneSuccesfulPollUsingFileFilter() throws Exception {
        // given 
        Mockito.when(directoryMock.listFiles())
                .thenReturn(list("a.txt/12", "b.xml/11"));

        final Set<FileElement> files = new LinkedHashSet<FileElement>();
        Mockito.doAnswer(new Answer<InitialContentEvent>() {

            @Override
            public InitialContentEvent answer(InvocationOnMock invocation) throws Throwable {
                Set<FileElement> s = ((InitialContentEvent) invocation.getArguments()[0]).getFiles();
                files.addAll(s);
                return null;
            }
        }).when(listenerMock).initialContent(Mockito.any(InitialContentEvent.class));

        // when
        dp = builder
                .addListener(new PollCycleCounter().stopPollingAfterNumOfCycles(1))
                .addListener(listenerMock)
                .addPolledDirectory(directoryMock)
                .setDefaultFileFilter(new RegexFileFilter(".*\\.txt"))
                .setThreadName("NAME")
                .setPollingInterval(20, TimeUnit.MILLISECONDS)
                .start();
        dp.awaitTermination();

        // then
        Assertions.assertThat(files).containsExactly(array("a.txt/12"));
        Assertions.assertThat(dp.getThreadName()).isEqualTo("NAME");
        Assertions.assertThat(dp.getPollingIntervalInMillis()).isEqualTo(20);
        verifyEventsInOrder(
                BeforeStartEvent.class,
                BeforePollingCycleEvent.class,
                InitialContentEvent.class,
                AfterPollingCycleEvent.class,
                AfterStopEvent.class);
        Mockito.verifyNoMoreInteractions(listenerMock);
    }
}