package org.araymond.joal.core.torrent.watcher;

import org.apache.commons.io.monitor.FileAlterationListenerAdaptor;
import org.araymond.joal.core.utils.TorrentFileCreator;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.File;
import java.io.IOException;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Comparator;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import static org.assertj.core.api.Assertions.*;

/**
 * Created by raymo on 01/05/2017.
 */
public class TorrentFileWatcherTest {

    private static final Path torrentsPath = Paths.get("src/test/resources/configtest").resolve("torrents");

    private void resetDirectories() throws IOException {
        if (Files.exists(torrentsPath)) {
            Files.walk(torrentsPath, FileVisitOption.FOLLOW_LINKS)
                    .sorted(Comparator.reverseOrder())
                    .map(Path::toFile)
                    .forEach(File::delete);
        }
        Files.createDirectory(torrentsPath);
    }

    @AfterEach
    @BeforeEach
    public void setUpAndTearDown() throws IOException {
        resetDirectories();
    }

    @Test
    public void shouldNotBuildWithNullListener() {
        assertThatThrownBy(() -> new TorrentFileWatcher(null, torrentsPath))
                .isInstanceOf(NullPointerException.class)
                .hasMessageContaining("listener cannot be null");
    }

    @Test
    public void shouldNotBuildWithNullMonitoredFolder() {
        assertThatThrownBy(() -> new TorrentFileWatcher(new FileAlterationListenerAdaptor(), null))
                .isInstanceOf(NullPointerException.class)
                .hasMessageContaining("monitoredFolder cannot be null");
    }

    @Test
    public void shouldNotBuildWithNonExistingMonitoredFolder() {
        assertThatThrownBy(() -> new TorrentFileWatcher(new FileAlterationListenerAdaptor(), torrentsPath.resolve("nop")))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("Folder [" + torrentsPath.resolve("nop").toAbsolutePath() + "] does not exists.");
    }

    @Test
    public void shouldNotBuildWithIntervalLessThan1() {
        assertThatThrownBy(() -> new TorrentFileWatcher(new FileAlterationListenerAdaptor(), torrentsPath, 0))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("intervalMs cannot be less than 1");
    }

    @Test
    public void shouldBuild() {
        try {
            final TorrentFileWatcher watcher = new TorrentFileWatcher(new FileAlterationListenerAdaptor(), torrentsPath);
            watcher.start();
            watcher.stop();
        } catch (final Throwable e) {
            fail("Should Not fail");
        }
    }

    @Test
    public void shouldOnlyDetectTorrentFiles() throws IOException, InterruptedException {
        Files.createFile(torrentsPath.resolve("loop.txt"));
        Files.createFile(torrentsPath.resolve("loop.torrent.txt"));

        final TorrentFileWatcher watcher = new TorrentFileWatcher(
                new FailOnTriggerListener(),
                torrentsPath,
                5
        );
        watcher.start();

        Files.createFile(torrentsPath.resolve("loop2.txt"));
        Files.createFile(torrentsPath.resolve("loop2.torrent.txt"));
        Thread.sleep(15);

        watcher.stop();
    }

    @Test
    public void shouldNotifyOnCreate() throws InterruptedException, IOException {
        final CountDownLatch createLock = new CountDownLatch(1);
        final CountDownLatch changeLock = new CountDownLatch(1);
        final CountDownLatch deleteLock = new CountDownLatch(1);
        final TorrentFileWatcher watcher = new TorrentFileWatcher(
                new CountDownLatchListener(createLock, changeLock, deleteLock),
                torrentsPath,
                5
        );
        watcher.start();

        TorrentFileCreator.create(torrentsPath.resolve("ubuntu.torrent"), TorrentFileCreator.TorrentType.UBUNTU);

        assertThat(createLock.await(100, TimeUnit.MILLISECONDS)).isTrue();
        watcher.stop();

        assertThat(createLock.getCount()).isEqualTo(0);
        assertThat(changeLock.getCount()).isEqualTo(1);
        assertThat(deleteLock.getCount()).isEqualTo(1);
    }

    @Test
    public void shouldNotifyOnChange() throws IOException, InterruptedException {
        final CountDownLatch createLock = new CountDownLatch(1);
        final CountDownLatch changeLock = new CountDownLatch(1);
        final CountDownLatch deleteLock = new CountDownLatch(1);
        final TorrentFileWatcher watcher = new TorrentFileWatcher(
                new CountDownLatchListener(createLock, changeLock, deleteLock),
                torrentsPath,
                5
        );
        watcher.start();

        final Path torrent = TorrentFileCreator.create(torrentsPath.resolve("ubuntu.torrent"), TorrentFileCreator.TorrentType.UBUNTU);
        assertThat(createLock.await(100, TimeUnit.MILLISECONDS)).isTrue();

        if (!torrent.toFile().setLastModified(100)) {
            fail("failed to modify file date");
        }
        assertThat(changeLock.await(100, TimeUnit.MILLISECONDS)).isTrue();
        watcher.stop();

        assertThat(createLock.getCount()).isEqualTo(0);
        assertThat(changeLock.getCount()).isEqualTo(0);
        assertThat(deleteLock.getCount()).isEqualTo(1);
    }

    @Test
    public void shouldNotifyOnDelete() throws IOException, InterruptedException {
        final CountDownLatch createLock = new CountDownLatch(1);
        final CountDownLatch changeLock = new CountDownLatch(1);
        final CountDownLatch deleteLock = new CountDownLatch(1);
        final TorrentFileWatcher watcher = new TorrentFileWatcher(
                new CountDownLatchListener(createLock, changeLock, deleteLock),
                torrentsPath,
                5
        );
        watcher.start();

        final Path torrent = TorrentFileCreator.create(torrentsPath.resolve("ubuntu.torrent"), TorrentFileCreator.TorrentType.UBUNTU);
        assertThat(createLock.await(100, TimeUnit.MILLISECONDS)).isTrue();

        Files.delete(torrent);
        assertThat(deleteLock.await(100, TimeUnit.MILLISECONDS)).isTrue();
        watcher.stop();

        assertThat(createLock.getCount()).isEqualTo(0);
        assertThat(changeLock.getCount()).isEqualTo(1);
        assertThat(deleteLock.getCount()).isEqualTo(0);
    }

    @Test
    public void shouldDetectFileAlreadyPresentOnStart() throws IOException, InterruptedException {
        TorrentFileCreator.create(torrentsPath.resolve("ubuntu.torrent"), TorrentFileCreator.TorrentType.UBUNTU);
        TorrentFileCreator.create(torrentsPath.resolve("ninja.torrent"), TorrentFileCreator.TorrentType.NINJA_HEAT);

        final CountDownLatch createLock = new CountDownLatch(2);
        final CountDownLatch changeLock = new CountDownLatch(1);
        final CountDownLatch deleteLock = new CountDownLatch(1);
        final TorrentFileWatcher watcher = new TorrentFileWatcher(
                new CountDownLatchListener(createLock, changeLock, deleteLock),
                torrentsPath,
                5
        );
        watcher.start();
        assertThat(createLock.await(100, TimeUnit.MILLISECONDS)).isTrue();
        watcher.stop();

        assertThat(createLock.getCount()).isEqualTo(0);
        assertThat(changeLock.getCount()).isEqualTo(1);
        assertThat(deleteLock.getCount()).isEqualTo(1);
    }

    @Test
    public void shouldNotWatchSubFolders() throws IOException, InterruptedException {
        Files.createDirectory(torrentsPath.resolve("sub-folder"));
        final TorrentFileWatcher watcher = new TorrentFileWatcher(
                new FailOnTriggerListener(),
                torrentsPath,
                5
        );
        watcher.start();

        TorrentFileCreator.create(torrentsPath.resolve("sub-folder").resolve("ubuntu.torrent"), TorrentFileCreator.TorrentType.UBUNTU);
        Thread.sleep(15);

        watcher.stop();
    }

    @Test
    public void shouldNotDetectFilesInSubFolderOnStart() throws IOException {
        Files.createDirectory(torrentsPath.resolve("sub-folder"));
        TorrentFileCreator.create(torrentsPath.resolve("sub-folder").resolve("ubuntu.torrent"), TorrentFileCreator.TorrentType.UBUNTU);
        final TorrentFileWatcher watcher = new TorrentFileWatcher(
                new FailOnTriggerListener(),
                torrentsPath,
                5
        );
        watcher.start();
        watcher.stop();
    }

    @Test
    public void shouldFaiToStartWithNonExistingPath() throws IOException {
        final Path directory = Files.createDirectory(torrentsPath.resolve("sub-folder"));
        final TorrentFileWatcher torrentFileWatcher = new TorrentFileWatcher(
                new FileAlterationListenerAdaptor(),
                directory,
                10
        );

        Files.delete(directory);

        assertThatThrownBy(torrentFileWatcher::start)
                .isInstanceOf(IllegalStateException.class);
    }

    private static final class FailOnTriggerListener extends FileAlterationListenerAdaptor {

        @Override
        public void onFileCreate(final File file) {
            fail("Should not have had detected");
        }

        @Override
        public void onFileChange(final File file) {
            fail("Should not have had detected");
        }

        @Override
        public void onFileDelete(final File file) {
            fail("Should not have had detected");
        }
    }

    private static final class CountDownLatchListener extends FileAlterationListenerAdaptor {

        private final CountDownLatch createLock;
        private final CountDownLatch changeLock;
        private final CountDownLatch deleteLock;

        private CountDownLatchListener(final CountDownLatch createLock, final CountDownLatch changeLock, final CountDownLatch deleteLock) {
            this.createLock = createLock;
            this.changeLock = changeLock;
            this.deleteLock = deleteLock;
        }

        @Override
        public void onFileCreate(final File file) {
            createLock.countDown();
        }

        @Override
        public void onFileChange(final File file) {
            changeLock.countDown();
        }

        @Override
        public void onFileDelete(final File file) {
            deleteLock.countDown();
        }
    }

}
