package org.peerbox.watchservice;

import java.io.File;
import java.io.FileWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.apache.commons.io.FileUtils;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.peerbox.app.manager.file.IFileManager;
import org.peerbox.testutils.WatchServiceTestHelpers;
import org.peerbox.watchservice.filetree.FileTree;
import org.peerbox.watchservice.integration.TestPeerWaspConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class NativeFolderWatchServiceTest {

	private static final Logger logger = LoggerFactory.getLogger(NativeFolderWatchServiceTest.class);
	private FolderWatchService watchService;
	private FileEventManager eventManager;
	private ActionExecutor actionExecutor;
	private FileTree fileTree;

	@Mock
	private IFileManager fileManager;
	private static Path basePath;

	private static final int NUM_CHARS_SMALL_FILE = 50*1024;
	private static final int NUM_CHARS_BIG_FILE = 50*1024*1024;
//	private static final int SLEEP_TIME = 4000;

	@BeforeClass
	public static void setup() throws Exception {
		basePath = Paths.get(FileUtils.getTempDirectoryPath(), "PeerWasp_FolderWatchServiceTest");
		basePath.toFile().mkdir();
		logger.info("Path: {}", basePath);

	}

	@AfterClass
	public static void teardown() {

	}

	@Before
	public void initialization() throws Exception {
		FileUtils.cleanDirectory(basePath.toFile());

		MockitoAnnotations.initMocks(this);

		watchService = new FolderWatchService();
		fileTree = new FileTree(basePath, true);
		eventManager = new FileEventManager(fileTree, null);
		actionExecutor = new ActionExecutor(eventManager, fileManager, new TestPeerWaspConfig());
		actionExecutor.setWaitForActionCompletion(false);
		actionExecutor.start();
		watchService.addFileEventListener(eventManager);

		logger.info("Running");
	}

	@After
	public void cleanFolder() throws Exception {
		watchService.stop();
		Thread.sleep(500);
		FileUtils.cleanDirectory(basePath.toFile());
	}

	private void sleep() throws InterruptedException {
		Thread.sleep(actionExecutor.getPeerWaspConfig().getAggregationIntervalInMillis() * 2);
	}


	/**
	 * Create a file and test whether it gets handled by the
	 * handleCreateEvent and eventually send over to H2H.add
	 *
	 * @throws Exception
	 */
	@Test
	public void testFileCreate() throws Exception {
		watchService.start(basePath);

		// create new file
		File add = Paths.get(basePath.toString(), "add_empty.txt").toFile();
		add.createNewFile();
		sleep();

		Mockito.verify(fileManager, Mockito.times(1)).add(add.toPath());
	}

	/**
	 * Create a file with some data in it and test whether
	 * it gets handled by the handleCreateEvent
	 *
	 * @throws Exception
	 */
	@Test
	public void testSmallFileCreate() throws Exception {
		watchService.start(basePath);

		// create new small file
		File add = Paths.get(basePath.toString(), "add_small.txt").toFile();

		FileWriter out = new FileWriter(add);
		WatchServiceTestHelpers.writeRandomData(out, NUM_CHARS_SMALL_FILE);
		out.close();
		sleep();

		Mockito.verify(fileManager, Mockito.times(1)).add(add.toPath());
	}

	/**
	 * Create a file which contains a large portion of data and test
	 * whether it gets handled by the handleCreateEvent
	 *
	 * @throws Exception
	 */
	@Test
	public void testBigFileCreate() throws Exception {
		watchService.start(basePath);

		// create new big file
		File add = Paths.get(basePath.toString(), "add_big.txt").toFile();

		FileWriter out = new FileWriter(add);
		WatchServiceTestHelpers.writeRandomData(out, NUM_CHARS_BIG_FILE);
		out.close();
		sleep();

		Mockito.verify(fileManager, Mockito.times(1)).add(add.toPath());
	}

	/**
	 * Test whether a file which already was added to H2H does not
	 * lead to a hard-delete when it is desynchronized.
	 *
	 * @throws Exception
	 */
	@Test
	public void testFileSoftDelete() throws Exception {
		// create new file
		File delete = Paths.get(basePath.toString(), "delete_empty.txt").toFile();
		delete.createNewFile();
		sleep();

		watchService.start(basePath);
		//delete newly created file
		FileUtils.forceDelete(delete);
		sleep();

		Mockito.verifyNoMoreInteractions(fileManager);
	}

	/**
	 * Test whether a modify event will be detected when a file
	 * gets altered
	 *
	 * @throws Exception
	 */
	@Test
	public void testFileModify() throws Exception {
		watchService.start(basePath);

		// create new small file
		File modify = Paths.get(basePath.toString(), "modify_small.txt").toFile();

		FileWriter out = new FileWriter(modify);
		WatchServiceTestHelpers.writeRandomData(out, NUM_CHARS_SMALL_FILE);
		out.close();
		sleep();
		Mockito.verify(fileManager, Mockito.times(1)).add(modify.toPath());

		// modify newly created file
		out = new FileWriter(modify);
		WatchServiceTestHelpers.writeRandomData(out, NUM_CHARS_SMALL_FILE);
		out.close();
		sleep();

		Mockito.verify(fileManager, Mockito.times(1)).update(modify.toPath());
	}

	/**
	 * Test whether a modify event of a big file gets detected when a file
	 *  gets altered
	 *
	 * @throws Exception
	 */
	@Test
	public void testBigFileModify() throws Exception {
		watchService.start(basePath);

		// create new small file
		File modify = Paths.get(basePath.toString(), "modify_big.txt").toFile();

		FileWriter out = new FileWriter(modify);
		WatchServiceTestHelpers.writeRandomData(out, NUM_CHARS_BIG_FILE);
		out.close();
		sleep();
		Mockito.verify(fileManager, Mockito.times(1)).add(modify.toPath());

		// modify newly created file
		out = new FileWriter(modify);
		WatchServiceTestHelpers.writeRandomData(out, NUM_CHARS_BIG_FILE);
		out.close();
		sleep();

		Mockito.verify(fileManager).update(modify.toPath());

	}

	/**
	 * Test whether renaming a file gets recognized as an move event
	 * @throws Exception
	 */
	@Test
	public void testFileRename() throws Exception {
		watchService.start(basePath);

		File rename = Paths.get(basePath.toString(), "rename.txt").toFile();
		File newName = Paths.get(basePath.toString(), "rename_rename.txt").toFile();

		FileWriter out = new FileWriter(rename);
		WatchServiceTestHelpers.writeRandomData(out, NUM_CHARS_SMALL_FILE);
		out.close();
		sleep();

		Mockito.verify(fileManager, Mockito.times(1)).add(rename.toPath());

		rename.renameTo(newName);
		sleep();
		Mockito.verify(fileManager, Mockito.times(1)).move(rename.toPath(), newName.toPath());

	}

	/**
	 * Test whether the movement of a file to an existing sub directory gets recognized
	 * as an move event
	 *
	 * @throws Exception
	 */
	@Test
	public void testFileMove() throws Exception {
		watchService.start(basePath);

		// create new sub directory
		String subDirString = "subDir";
		File subDir = Paths.get(basePath.toString(), subDirString).toFile();
		FileUtils.forceMkdir(subDir);

		// create new file
		File file = Paths.get(basePath.toString(), "move.txt").toFile();
		// target file path
		File newFile = Paths.get(basePath.toString(), subDirString, "move.txt").toFile();

		FileWriter out = new FileWriter(file);
		WatchServiceTestHelpers.writeRandomData(out, NUM_CHARS_SMALL_FILE);
		out.close();
		sleep();

		Mockito.verify(fileManager, Mockito.times(1)).add(file.toPath());

		// move file to new target path
		FileUtils.moveFile(file, newFile);
		sleep();
		Mockito.verify(fileManager, Mockito.times(1)).move(file.toPath(), newFile.toPath());
	}

	/**
	 * Copy an existing file to another location and check whether
	 * the appropriate event gets triggered
	 *
	 * @throws Exception
	 */
	@Test
	public void testSmallFileCopy() throws Exception {
		File file = Paths.get(basePath.toString(), "original.txt").toFile();
		File newFile = Paths.get(basePath.toString(), "copy.txt").toFile();
		FileWriter out = new FileWriter(file);
		WatchServiceTestHelpers.writeRandomData(out, NUM_CHARS_SMALL_FILE);
		out.close();

		watchService.start(basePath);
		FileUtils.copyFile(file, newFile);
		sleep();
		Mockito.verify(fileManager, Mockito.times(1)).add(newFile.toPath());
		Mockito.verify(fileManager, Mockito.never()).delete(file.toPath());
		Mockito.verify(fileManager, Mockito.never()).move(file.toPath(), newFile.toPath());
	}

	@Test
	public void testBigFileCopy() throws Exception {
		File file = Paths.get(basePath.toString(), "original_big.txt").toFile();
		File newFile = Paths.get(basePath.toString(), "copy_big.txt").toFile();

		FileWriter out = new FileWriter(file);
		WatchServiceTestHelpers.writeRandomData(out, NUM_CHARS_BIG_FILE);
		out.close();

		watchService.start(basePath);
		FileUtils.copyFile(file, newFile);
		sleep();
		Mockito.verify(fileManager, Mockito.times(1)).add(newFile.toPath());
		Mockito.verify(fileManager, Mockito.never()).delete(file.toPath());
		Mockito.verify(fileManager, Mockito.never()).move(file.toPath(), newFile.toPath());
	}

	@Test
	public void testManySimultaneousEvents() throws Exception {
		watchService.start(basePath);

		List<File> folderList = new ArrayList<File>();
		List<File> fileList = new ArrayList<File>();

		int numberOfFolders = WatchServiceTestHelpers.randomInt(5, 50);
		int numberOfFiles = WatchServiceTestHelpers.randomInt(200, 1000);

		// create random folders & save files(paths) in list
		String folderName = null;

		for (int i = 1; i <= numberOfFolders; i++){

			folderName = WatchServiceTestHelpers.getRandomString(15, "abcdefghijklmnopqrstuvwxyz123456789");
			String subDirString = "\\" + folderName  +"\\";
			File subDir = Paths.get(basePath.toString(), subDirString).toFile();
			FileUtils.forceMkdir(subDir);

			folderList.add(subDir);
		}

		// create random files in base path & save files(paths) in list
		File randomFile;

		for (int i = 1; i <= numberOfFiles; i++){

			String randomFileName = WatchServiceTestHelpers.getRandomString(15, "abcdefghijklmnopqrstuvwxyz123456789");

			randomFile = Paths.get(basePath.toString(), randomFileName + ".txt").toFile();
			FileWriter out = new FileWriter(randomFile);
			WatchServiceTestHelpers.writeRandomData(out, NUM_CHARS_SMALL_FILE);
			out.close();

			fileList.add(randomFile);
		}

		// pick a random number of files and move them randomly to folders
		int randomNumberOfMovements = WatchServiceTestHelpers.randomInt(1, numberOfFiles);

		for (int i = 1; i <= randomNumberOfMovements; i++){

			// randomly pick a file from the fileList
			int randomFilePick = WatchServiceTestHelpers.randomInt(0, fileList.size()-1);
			File randomFileToMove = fileList.get(randomFilePick);

			// randomly pick a target sub directory
			int randomFolderPick = WatchServiceTestHelpers.randomInt(0, folderList.size()-1);
			File randomTargetFolder = folderList.get(randomFolderPick);

			FileUtils.moveFileToDirectory(randomFileToMove, randomTargetFolder, false);

			// remove moved file from file list
			fileList.remove(randomFileToMove);
		}

		sleep();

	//	Mockito.verify(fileManager, Mockito.times(randomNumberOfMovements)).move(Matchers.anyObject(), Matchers.anyObject());
	}


	@Test
	public void createManyFiles() throws Exception{
		int numFiles = 1000;
		watchService.start(basePath);
		List<Path> files = new ArrayList<Path>();

		while(numFiles > 0) {
			String randomFileName = WatchServiceTestHelpers.getRandomString(5, "abcdfg1234");
			Path file = Paths.get(basePath.toString(), randomFileName + ".txt");
			FileWriter out = new FileWriter(file.toFile());
			WatchServiceTestHelpers.writeRandomData(out, NUM_CHARS_SMALL_FILE);
			out.close();
			files.add(file);
			--numFiles;
		}
		sleep();

		Iterator<Path> it = files.iterator();
		while(it.hasNext()) {
			Mockito.verify(fileManager, Mockito.times(1)).add(it.next());
		}
	}


	@Test @Ignore
	public void testCopyFolder() throws Exception {
		// create folder with files
		int numFiles = 100;
		Path original = Paths.get(basePath.toString(), "original_folder");
		Files.createDirectory(original);

		List<Path> files = new ArrayList<Path>();
		Path copy = Paths.get(basePath.toString(), "copy_folder");
		files.add(original);
		for(int i = 0; i < numFiles; ++i) {
			Path f = Paths.get(original.toString(), String.format("file_%s.txt", i));
			FileWriter out = new FileWriter(f.toFile());
			WatchServiceTestHelpers.writeRandomData(out, NUM_CHARS_SMALL_FILE);
			out.close();
			files.add(Paths.get(copy.toString(), f.getFileName().toString()));

		}

		watchService.start(basePath);

		// copy folder
		FileUtils.copyDirectory(original.toFile(), copy.toFile());
		Thread.sleep(actionExecutor.getPeerWaspConfig().getAggregationIntervalInMillis() * 2);

		Mockito.verify(fileManager, Mockito.times(1)).add(copy);
//		Iterator<Path> it = files.iterator();
//		while(it.hasNext()) {
//			Path newFile = it.next();
////			Path newFile = Paths.get(copy.toString(), oldFile.getFileName().toString());
//			assertTrue(Files.exists(newFile));
//			Object obj = Mockito.when(fileManager.add(newFile)).thenReturn(null);
//
//			Mockito.verify(fileManager, Mockito.times(1)).add(newFile);
//			Mockito.verify(fileManager, Mockito.never()).delete(oldFile);
//			Mockito.verify(fileManager, Mockito.never()).move(oldFile, newFile);
		//}
		// TODO: test not implemented correctly yet
		//fail();


//		final int nrElements = files.size();
		final ArgumentCaptor<Path> captor = ArgumentCaptor.forClass(Path.class);
//
		Mockito.verify(fileManager).add(captor.capture());
//
//		// This is using assertJ
//
//		assertThat(captor.getAllValues()).isEqualTo(myObjects);
	}

	@Test
	public void testCreateManyFiles() throws Exception{
		watchService.start(basePath);
		File file = null;
		int fileNumbers = 1000;

		for (int i = 1; i <= fileNumbers; i++){

			String randomFileName = WatchServiceTestHelpers.getRandomString(9, "abcdefg123456789");

			file = Paths.get(basePath.toString(), randomFileName + ".txt").toFile();
			FileWriter out = new FileWriter(file);
			WatchServiceTestHelpers.writeRandomData(out, NUM_CHARS_SMALL_FILE);
			out.close();
			// System.out.println("File added: Itemnr.: " + i);
		}
		sleep();

		Mockito.verify(fileManager, Mockito.times(fileNumbers)).add(Matchers.anyObject());
	}

}