package com.github.davidmoten.rx;

import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchEvent.Kind;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import org.junit.Test;
import org.mockito.InOrder;
import org.mockito.Mockito;

import rx.Observable;
import rx.Observer;
import rx.Subscription;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.observers.TestSubscriber;
import rx.schedulers.Schedulers;

public class FileObservableTest {

    @Test
    public void testNoEventsThrownIfFileDoesNotExist() throws InterruptedException {
        File file = new File("target/does-not-exist");
        Observable<WatchEvent<?>> events = FileObservable.from(file, ENTRY_MODIFY);
        final CountDownLatch latch = new CountDownLatch(1);
        Subscription sub = events.subscribeOn(Schedulers.io())
                .subscribe(new Observer<WatchEvent<?>>() {

                    @Override
                    public void onCompleted() {
                        latch.countDown();
                    }

                    @Override
                    public void onError(Throwable e) {
                        latch.countDown();
                        e.printStackTrace();
                    }

                    @Override
                    public void onNext(WatchEvent<?> arg0) {
                        latch.countDown();
                    }
                });
        assertFalse(latch.await(100, TimeUnit.MILLISECONDS));
        sub.unsubscribe();
    }

    @Test
    public void testCreateAndModifyEventsForANonDirectoryFileBlockForever()
            throws InterruptedException, IOException {
        File file = new File("target/f");
        Observable<WatchEvent<?>> events = FileObservable.from(file).kind(ENTRY_MODIFY)
                .kind(ENTRY_CREATE).events();
        checkCreateAndModifyEvents(file, events);
    }

    @Test
    public void testCreateAndModifyEventsForANonDirectoryFilePollEveryInterval()
            throws InterruptedException, IOException {
        File file = new File("target/f");
        Observable<WatchEvent<?>> events = FileObservable.from(file).kind(ENTRY_MODIFY)
                .kind(ENTRY_CREATE).pollInterval(100, TimeUnit.MILLISECONDS).events();
        checkCreateAndModifyEvents(file, events);
    }

    @Test
    public void testCreateAndModifyEventsForANonDirectoryFileBlockingPollEveryInterval()
            throws InterruptedException, IOException {
        File file = new File("target/f");
        Observable<WatchEvent<?>> events = FileObservable.from(file).kind(ENTRY_MODIFY)
                .kind(ENTRY_CREATE).pollInterval(100, TimeUnit.MILLISECONDS)
                .pollDuration(100, TimeUnit.MILLISECONDS).events();
        checkCreateAndModifyEvents(file, events);
    }

    private void checkCreateAndModifyEvents(File file, Observable<WatchEvent<?>> events)
            throws InterruptedException, IOException, FileNotFoundException {
        file.delete();
        final CountDownLatch latch = new CountDownLatch(1);
        @SuppressWarnings("unchecked")
        final List<Kind<?>> eventKinds = Mockito.mock(List.class);
        InOrder inOrder = Mockito.inOrder(eventKinds);
        final AtomicInteger errorCount = new AtomicInteger(0);
        Subscription sub = events.subscribeOn(Schedulers.io())
                .subscribe(new Observer<WatchEvent<?>>() {

                    @Override
                    public void onCompleted() {
                        System.out.println("completed");
                    }

                    @Override
                    public void onError(Throwable e) {
                        errorCount.incrementAndGet();
                    }

                    @Override
                    public void onNext(WatchEvent<?> event) {
                        System.out.println("event=" + event);
                        eventKinds.add(event.kind());
                        latch.countDown();
                    }
                });
        // sleep long enough for WatchService to start
        Thread.sleep(1000);
        file.createNewFile();
        FileOutputStream fos = new FileOutputStream(file, true);
        fos.write("hello there".getBytes());
        fos.close();
        // give the WatchService time to register the change
        Thread.sleep(100);
        assertTrue(latch.await(30000, TimeUnit.MILLISECONDS));
        inOrder.verify(eventKinds).add(StandardWatchEventKinds.ENTRY_CREATE);
        inOrder.verify(eventKinds).add(StandardWatchEventKinds.ENTRY_MODIFY);
        inOrder.verifyNoMoreInteractions();

        sub.unsubscribe();
        Thread.sleep(100);
        assertEquals(0, errorCount.get());
    }

    @Test
    public void testFileTailingFromStartOfFile() throws InterruptedException, IOException {
        final File log = new File("target/test.log");
        log.delete();
        log.createNewFile();
        append(log, "a0");

        Observable<String> tailer = FileObservable.tailer().file(log).onWatchStarted(new Action0() {
            @Override
            public void call() {
                append(log, "a1");
                append(log, "a2");
            }
        }).sampleTimeMs(50).utf8().tailText();
        final List<String> list = new ArrayList<String>();
        final CountDownLatch latch = new CountDownLatch(3);
        Subscription sub = tailer.subscribeOn(Schedulers.io()).subscribe(new Action1<String>() {
            @Override
            public void call(String line) {
                System.out.println("received: '" + line + "'");
                list.add(line);
                latch.countDown();
            }
        });
        assertTrue(latch.await(10, TimeUnit.SECONDS));
        assertEquals(Arrays.asList("a0", "a1", "a2"), list);
        sub.unsubscribe();
    }

    @Test
    public void testFileTailingWhenFileIsCreatedAfterSubscription()
            throws InterruptedException, IOException {
        final File log = new File("target/test.log");
        log.delete();

        append(log, "a0");
        Observable<String> tailer = FileObservable.tailer().file(log).startPosition(0)
                .sampleTimeMs(50).utf8().onWatchStarted(new Action0() {
                    @Override
                    public void call() {
                        try {
                            log.createNewFile();
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                        append(log, "a1");
                        append(log, "a2");
                    }
                }).tailText();

        final List<String> list = new ArrayList<String>();
        final CountDownLatch latch = new CountDownLatch(3);
        Subscription sub = tailer.subscribeOn(Schedulers.io()).subscribe(new Action1<String>() {
            @Override
            public void call(String line) {
                System.out.println("received: '" + line + "'");
                list.add(line);
                latch.countDown();
            }
        });
        assertTrue(latch.await(10, TimeUnit.SECONDS));
        assertEquals(Arrays.asList("a0", "a1", "a2"), list);
        sub.unsubscribe();
    }

    private static void append(File file, String line) {
        try {
            FileOutputStream fos = new FileOutputStream(file, true);
            fos.write(line.getBytes(Charset.forName("UTF-8")));
            fos.write('\n');
            fos.close();
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Test
    public void testTailTextFileStreamsFromEndOfFileIfSpecified()
            throws FileNotFoundException, InterruptedException {
        File file = new File("target/test1.txt");
        file.delete();
        try (PrintStream out = new PrintStream(file)) {
            out.println("line 1");
        }
        final List<String> list = new ArrayList<String>();
        TestSubscriber<String> ts = TestSubscriber.create();
        FileObservable.tailer().file(file).startPosition(file.length()).sampleTimeMs(10).utf8()
                .tailText()
                // for each
                .doOnNext(new Action1<String>() {

                    @Override
                    public void call(String line) {
                        System.out.println(line);
                        list.add(line);
                    }
                }).subscribeOn(Schedulers.newThread()).subscribe(ts);
        Thread.sleep(1100);
        assertTrue(list.isEmpty());
        try (PrintStream out = new PrintStream(new FileOutputStream(file, true))) {
            out.println("line 2");
        }
        Thread.sleep(1100);
        assertEquals(1, list.size());
        assertEquals("line 2", list.get(0).trim());
        ts.unsubscribe();
    }

    @Test
    public void testTailTextFileStreamsFromEndOfFileIfDeleteOccurs()
            throws InterruptedException, IOException {
        File file = new File("target/test2.txt");
        file.delete();
        try (PrintStream out = new PrintStream(file)) {
            out.println("line 1");
        }
        final List<String> list = new ArrayList<String>();
        Subscription sub = FileObservable.tailer().file(file).startPosition(file.length())
                .sampleTimeMs(10).utf8().tailText()
                // for each
                .doOnNext(new Action1<String>() {

                    @Override
                    public void call(String line) {
                        System.out.println(line);
                        list.add(line);
                    }
                }).subscribeOn(Schedulers.newThread()).subscribe();
        // delay must be long enough for last update timestamp to change on
        // windows (resolution to the second)
        Thread.sleep(1100);
        assertTrue(list.isEmpty());
        // delete file then make it bigger than it was
        assertTrue(file.delete());
        try (PrintStream out = new PrintStream(new FileOutputStream(file, true))) {
            out.println("line 2");
            out.println("line 3");
        }
        Thread.sleep(1100);
        assertEquals(2, list.size());
        assertEquals("line 2", list.get(0).trim());
        assertEquals("line 3", list.get(1).trim());
        sub.unsubscribe();
    }
}