package org.peerbox.watchservice;

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.BlockingQueue;

import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.Mockito;
import org.peerbox.app.manager.file.IFileManager;
import org.peerbox.testutils.FileTestUtils;
import org.peerbox.watchservice.filetree.FileTree;
import org.peerbox.watchservice.filetree.composite.FileComponent;
import org.peerbox.watchservice.integration.TestPeerWaspConfig;
import org.peerbox.watchservice.states.EstablishedState;
import org.peerbox.watchservice.states.InitialState;
import org.peerbox.watchservice.states.LocalCreateState;
import org.peerbox.watchservice.states.LocalHardDeleteState;
import org.peerbox.watchservice.states.LocalMoveState;
import org.peerbox.watchservice.states.LocalUpdateState;

import com.google.common.collect.SetMultimap;
import com.google.common.io.Files;

/**
 *
 * @author Claudio
 * This test creates a new directory in the user's home directory, where some files are created for
 * test purposes. The directory and the files are deleted after the execution of the testcases. This class
 * tests if the events triggered by the FolderWatchService are correctly aggregated and delivered to the H2H
 * framework, whereas the FileManager is mocked to decouple the test from the filesharing library
 */
public class FileEventManagerTest {

	private static int nrFiles = 9;
	private static FileTree fileTree;
	private static FileEventManager manager;
	private static ActionExecutor actionExecutor;
	private static IFileManager fileManager;

	private static String parentPath = System.getProperty("user.home") + File.separator + "PeerWasp_FileEventManagerTest" + File.separator;
	private static File testDirectory;
	private static ArrayList<String> filePaths = new ArrayList<String>();
	private static ArrayList<File> files = new ArrayList<File>();

	private TestPeerWaspConfig config = new TestPeerWaspConfig();


	/**
	 * Create the test directory and the files.
	 */
	@BeforeClass
	public static void staticSetup(){
		fileTree = new FileTree(Paths.get(parentPath), true);
		manager = new FileEventManager(fileTree, null);
		fileManager = Mockito.mock(IFileManager.class);
		actionExecutor = new ActionExecutor(manager, fileManager, new TestPeerWaspConfig());
		actionExecutor.setWaitForActionCompletion(false);
		actionExecutor.start();
	}

	/**
	 * Delete the test directory and the files.
	 */
	@AfterClass
	public static void rollback(){
		for(int i = 0; i < nrFiles; i++){
			files.get(i).delete();
		}
		assertTrue(testDirectory.delete());
	}

	@Before
	public void setup(){
		testDirectory = new File(parentPath);
		testDirectory.mkdir();
		try {
			for(int i = 0; i < nrFiles; i++){
				filePaths.add(parentPath + "file" + i + ".txt");
				files.add(new File(filePaths.get(i)));
				files.get(i).createNewFile();
				System.out.println("Created file " + files.get(i));
			}

		} catch (IOException e) {
			e.printStackTrace();
		}

	}

	/**
	 * Triggers a create event and a modify event on the same file. Ensures
	 * that only one element is stored in the action queue. This element
	 * has to be still in the create state after this two events are processed.
	 */
	@Test
	public void onFileCreatedTest(){
		BlockingQueue<FileComponent> fileComponentsToCheck = manager.getFileComponentQueue().getQueue();

		long start = System.currentTimeMillis();
		System.out.println("Start onFileCreatedTest");
		manager.onLocalFileCreated(Paths.get(filePaths.get(0)));
		assertTrue(fileComponentsToCheck.size() == 1);
		assertTrue(fileComponentsToCheck.peek().getAction().getCurrentState() instanceof LocalCreateState);

		manager.onLocalFileModified(Paths.get(filePaths.get(0)));
		assertTrue(fileComponentsToCheck.size() == 1);
		assertTrue(fileComponentsToCheck.peek().getAction().getCurrentState() instanceof LocalCreateState);

		FileComponent file = fileComponentsToCheck.peek();
		//check if the testcase was run in time
		long end = System.currentTimeMillis();
		assertTrue(end - start <= config.getAggregationIntervalInMillis());

		sleepMillis(config.getAggregationIntervalInMillis() * 2);

		assertTrue(file.getAction().getCurrentState() instanceof EstablishedState);
		assertTrue(fileComponentsToCheck.size() == 0);

		//cleanup
		manager.onLocalFileHardDelete(Paths.get(filePaths.get(0)));
		sleepMillis(200); //wait for the state machine before delete is simulated
		manager.onLocalFileDeleted(Paths.get(filePaths.get(0)));
		sleepMillis(config.getAggregationIntervalInMillis() * 2);
		System.out.println("Current state: " + file.getAction().getCurrentState().getClass());
		assertTrue(manager.getFileTree().getFile(files.get(0).toPath()) == null);
		assertTrue(file.getAction().getCurrentState() instanceof InitialState);
		assertTrue(fileComponentsToCheck.size() == 0);
	}

	/**
	 * This test simulates a create event and waits ActionExecutor.ACTION_WAIT_TIME_MS amount of
	 * time for the event to be handled. After that, a move is simulated using a delete event on
	 * the same file and a create event on a new file with the same content (but different name).
	 */
	@Test
	public void fromDeleteToMoveTest(){
		//handle artificial create event, wait for handling
		manager.onLocalFileCreated(Paths.get(filePaths.get(7)));
		BlockingQueue<FileComponent> actionsToCheck = manager.getFileComponentQueue().getQueue();;
		FileComponent file1 = actionsToCheck.peek();
		sleepMillis(config.getAggregationIntervalInMillis() * 2);

		//check if exactly one element exists in the queue
		assertTrue(actionsToCheck.size() == 0);

		//initiate delete event
		long start = System.currentTimeMillis();

		manager.onLocalFileDeleted(Paths.get(filePaths.get(7)));
		assertTrue(actionsToCheck.size() == 1);

		//initiate re-creation, ensure that all happens in time
		manager.onLocalFileCreated(Paths.get(filePaths.get(8)));
		FileComponent file2 = actionsToCheck.peek();
		assertTrue(actionsToCheck.size() == 1);
		System.out.println(actionsToCheck.peek().getAction().getCurrentState().getClass());
		assertTrue(actionsToCheck.peek().getAction().getCurrentState() instanceof LocalMoveState);

		long end = System.currentTimeMillis();
		assertTrue(end - start <= config.getAggregationIntervalInMillis());
		sleepMillis(config.getAggregationIntervalInMillis() * 2);

		//cleanup
		deleteFile(Paths.get(filePaths.get(8)));
		sleepMillis(config.getAggregationIntervalInMillis() * 2);
		assertTrue(manager.getFileTree().getFile(files.get(8).toPath()) == null);
		assertTrue(actionsToCheck.size() == 0);
		assertTrue(file1.getAction().getCurrentState() instanceof InitialState);
		assertTrue(file1.getAction().getCurrentState() instanceof InitialState);
	}

	/**
	 * Simulate a file delete and an additional modify event, check if the file
	 * remains in the delete state and only one action is stored in the queue.
	 */
	@Test
	public void onFileDeletedTest(){
		BlockingQueue<FileComponent> actionsToCheck = manager.getFileComponentQueue().getQueue();;
		SetMultimap<String, FileComponent> deletedFiles = manager.getFileTree().getDeletedByContentHash();
		System.out.println("Start onFileDeletedTest");
		manager.onLocalFileCreated(Paths.get(filePaths.get(0)));
		FileComponent createdFile = actionsToCheck.peek();
		//HERE
		assertTrue(actionsToCheck.size() == 1);
		assertTrue(createdFile.getAction().getCurrentState() instanceof LocalCreateState);

		sleepMillis(config.getAggregationIntervalInMillis() * 2);

		assertTrue(createdFile.getAction().getCurrentState() instanceof EstablishedState);
		assertTrue(actionsToCheck.size() == 0);

		long start = System.currentTimeMillis();

		manager.onLocalFileHardDelete(Paths.get(filePaths.get(0)));
		sleepMillis(200);
		manager.onLocalFileDeleted(Paths.get(filePaths.get(0)));
		System.out.println(actionsToCheck.size());
		assertTrue(actionsToCheck.size() == 1);
		assertTrue(actionsToCheck.peek().getAction().getCurrentState() instanceof LocalHardDeleteState);

		manager.onLocalFileModified(Paths.get(filePaths.get(0)));
		assertTrue(actionsToCheck.size() == 1);
		assertTrue(actionsToCheck.peek().getAction().getCurrentState() instanceof LocalHardDeleteState);

		System.out.println("deletedFiles.size(): " + deletedFiles.size());
		//assertTrue(deletedFiles.size() == 1);
		//Set<FileComponent> equalHashes = deletedFiles.get(createdFile.getContentHash());
		//assertTrue(equalHashes.size() == 1);
		//assertTrue(equalHashes.contains(createdFile));

		//check if the testcase was run in time
		long end = System.currentTimeMillis();
		assertTrue(end - start <= config.getAggregationIntervalInMillis());



		sleepMillis(config.getAggregationIntervalInMillis() * 5);

		assertTrue(actionsToCheck.size() == 0);
		assertTrue(manager.getFileTree().getFile(files.get(0).toPath()) == null);
		System.out.println(createdFile.getAction().getCurrentState().getClass());
		assertTrue(createdFile.getAction().getCurrentState() instanceof InitialState);
		System.out.println(actionsToCheck.size());
		assertTrue(deletedFiles.size() == 0);
	}

	/**
	 * This test issues several modify events for the same file over a long
	 * period to check if the events are aggregated accordingly.
	 * @throws IOException
	 */
	@Test
	public void onFileModifiedTest() throws IOException{
		BlockingQueue<FileComponent> actionsToCheck = manager.getFileComponentQueue().getQueue();;

		long start = System.currentTimeMillis();
		System.out.println("Start onFileModifiedTest");
		manager.onLocalFileCreated(Paths.get(filePaths.get(0)));
		manager.onLocalFileModified(Paths.get(filePaths.get(0)));
		assertTrue(actionsToCheck.size() == 1);
		assertNotNull(actionsToCheck);
		assertNotNull(actionsToCheck.peek());
		assertNotNull(actionsToCheck.peek().getAction().getCurrentState());
		assertTrue(actionsToCheck.peek().getAction().getCurrentState() instanceof LocalCreateState); //no null pointers should occur anymore here

		long end = System.currentTimeMillis();

		assertTrue(end - start <= config.getAggregationIntervalInMillis());

		//issue continuous modifies over a period longer than the wait time
		sleepMillis(config.getAggregationIntervalInMillis() * 2);

		FileTestUtils.writeRandomData(files.get(0).toPath(), 50);
		manager.onLocalFileModified(Paths.get(filePaths.get(0)));
		sleepMillis(config.getAggregationIntervalInMillis() / 2);

		FileTestUtils.writeRandomData(files.get(0).toPath(), 50);
		manager.onLocalFileModified(Paths.get(filePaths.get(0)));
		sleepMillis(config.getAggregationIntervalInMillis() / 2);

		FileComponent comp = actionsToCheck.peek();
		assertTrue(actionsToCheck.peek().getAction().getCurrentState() instanceof LocalUpdateState);
		assertTrue(actionsToCheck.size() == 1);

		sleepMillis(config.getAggregationIntervalInMillis() * 2);
		printBlockingQueue(actionsToCheck);
		assertTrue(actionsToCheck.size() == 0);
	//	System.out.println(comp.getAction().getCurrentState().getClass());

		//cleanup
		manager.onLocalFileHardDelete(Paths.get(filePaths.get(0)));
		sleepMillis(200);
		manager.onLocalFileDeleted(Paths.get(filePaths.get(0)));
		sleepMillis(config.getAggregationIntervalInMillis() * 5);
		assertTrue(manager.getFileTree().getFile(files.get(0).toPath()) == null);
		assertTrue(comp.getAction().getCurrentState() instanceof InitialState);
		assertTrue(actionsToCheck.size() == 0);

	}

	private void printBlockingQueue(BlockingQueue<FileComponent> queue){
		System.out.println("Queue:");
		ArrayList<FileComponent> components = new ArrayList<FileComponent>(queue);
		int i = 0;
		for(FileComponent comp : components){
			System.out.println(i + ": " + comp.getAction().getCurrentState().getClass() + ": " + comp.getPath());
		}
	}

	/**
	 * Advanced test with four files and different events on them.
	 *
	 * The different events:
	 * - modify file0
	 * - create file1
	 * - delete file0
	 * - modify file2
	 * - delete file2
	 * - create file3 > move from file2 to file3
	 *
	 * Expected action queue content
	 *
	 * (tail) [move file2 to file3], [delete file0] [create file1] (head)
	 * @throws IOException
	 */

	@Test
	public void multipleFilesTest() throws IOException{
		//measure start time to ensure the testcase runs before the queue is processed
		System.out.println("Start multipleFilesTest");
		BlockingQueue<FileComponent> actionsToCheck = manager.getFileComponentQueue().getQueue();;

		//issue all the events, check state of head and if the action corresponds to the correct file
		manager.onLocalFileCreated(Paths.get(filePaths.get(0)));
		//manager.onFileCreated(Paths.get(filePaths.get(1)), false);
		manager.onLocalFileCreated(Paths.get(filePaths.get(2)));
		//manager.onFileCreated(Paths.get(filePaths.get(3)), false);
		sleepMillis(config.getAggregationIntervalInMillis() * 2);
		long start = System.currentTimeMillis();
		FileTestUtils.writeRandomData(files.get(0).toPath(), 50);
		manager.onLocalFileModified(Paths.get(filePaths.get(0)));
		sleepMillis(50);
		printQueue(actionsToCheck);
		assertTrue(actionsToCheck.size() == 1);
		assertTrue(actionsToCheck.peek().getAction().getCurrentState() instanceof LocalUpdateState);
		assertTrue(actionsToCheck.peek().getPath().toString().equals(filePaths.get(0)));

		manager.onLocalFileCreated(Paths.get(filePaths.get(1)));
		sleepMillis(10);
		assertTrue(actionsToCheck.size() == 2);
		assertTrue(actionsToCheck.peek().getAction().getCurrentState() instanceof LocalUpdateState);
		assertTrue(actionsToCheck.peek().getPath().toString().equals(filePaths.get(0)));


		manager.onLocalFileHardDelete(Paths.get(filePaths.get(0)));
		sleepMillis(200);
		manager.onLocalFileDeleted(Paths.get(filePaths.get(0)));
		sleepMillis(10);
		System.out.println("actionsToCheck.size() " + actionsToCheck.size());
		ArrayList<FileComponent> array = new ArrayList<FileComponent>(actionsToCheck);
		for(FileComponent comp : array){
			System.out.println(comp.getPath() + ": " + comp.getAction().getCurrentState().getClass().toString());
		}
		assertTrue(actionsToCheck.size() == 2);
		assertTrue(actionsToCheck.peek().getAction().getCurrentState() instanceof LocalCreateState);
		assertTrue(actionsToCheck.peek().getPath().toString().equals(filePaths.get(1)));


		FileTestUtils.writeRandomData(files.get(2).toPath(), 50);
		manager.onLocalFileModified(Paths.get(filePaths.get(2)));

		sleepMillis(10);
		System.out.println("actionsToCheck.size() " + actionsToCheck.size());
		assertTrue(actionsToCheck.size() == 3);
		assertTrue(actionsToCheck.peek().getAction().getCurrentState() instanceof LocalCreateState);
		assertTrue(actionsToCheck.peek().getPath().toString().equals(filePaths.get(1)));
		Files.move(files.get(2), files.get(3));

		manager.onLocalFileDeleted(Paths.get(filePaths.get(2)));
		sleepMillis(10);
		System.out.println("size: " + actionsToCheck.size());

		Vector<FileComponent> actions = new Vector<FileComponent>(actionsToCheck);
		for(int i = 0; i < actions.size(); i++){
			System.out.println(i + ": " + actions.get(i).getPath() + " - " + actions.get(i).getAction().getCurrentState().getClass());
		}

		assertTrue(actionsToCheck.size() == 3);
		assertTrue(actionsToCheck.peek().getAction().getCurrentState() instanceof LocalCreateState);
		assertTrue(actionsToCheck.peek().getPath().toString().equals(filePaths.get(1)));

		manager.onLocalFileCreated(Paths.get(filePaths.get(3)));
		sleepMillis(10);
		System.out.println("actionsToCheck.size() " + actionsToCheck.size());
		assertTrue(actionsToCheck.size() == 3);
		assertTrue(actionsToCheck.peek().getAction().getCurrentState() instanceof LocalCreateState);
		assertTrue(actionsToCheck.peek().getPath().toString().equals(filePaths.get(1)));

		List<FileComponent> actionsList = new ArrayList<FileComponent>(actionsToCheck);

		//poll elements from the queue, check state and file path for each of them
		FileComponent head = actionsList.get(0);
		assertTrue(actionsToCheck.size() == 3);
		assertTrue(head.getAction().getCurrentState() instanceof LocalCreateState);
		assertTrue(head.getPath().toString().equals(filePaths.get(1)));

		head = actionsList.get(1);
		assertTrue(head.getAction().getCurrentState() instanceof LocalHardDeleteState);
		assertTrue(head.getPath().toString().equals(filePaths.get(0)));

		head = actionsList.get(2);
		assertTrue(head.getAction().getCurrentState() instanceof LocalMoveState);
		System.out.println("head.getAction().getFilePath().toString(): " + head.getPath().toString());
		System.out.println("filePaths.get(3): " + filePaths.get(3));
		assertTrue(head.getPath().toString().equals(filePaths.get(3)));

		long end = System.currentTimeMillis();

		assertTrue(end - start <= config.getAggregationIntervalInMillis());

		//cleanup:

//		deleteFile(Paths.get(filePaths.get(0)));
		deleteFile(Paths.get(filePaths.get(1)));
//		deleteFile(Paths.get(filePaths.get(2)));
		deleteFile(Paths.get(filePaths.get(3)));
		assertTrue(manager.getFileTree().getFile(files.get(0).toPath()) == null);
		assertTrue(manager.getFileTree().getFile(files.get(1).toPath()) == null);
		assertTrue(manager.getFileTree().getFile(files.get(2).toPath()) == null);
		assertTrue(manager.getFileTree().getFile(files.get(3).toPath()) == null);

		sleepMillis(config.getAggregationIntervalInMillis() * 5);
	}

	private void printQueue(BlockingQueue<FileComponent> queue) {
		Vector<FileComponent> files = new Vector<FileComponent>(queue);
		for(int i = 0; i < files.size(); i++){
			System.out.println(i + ". File :" + files.get(i).getPath() + " - " + files.get(i).getAction().getCurrentState());
		}
	}

	private void deleteFile(Path filePath){
		System.out.println("Hard delete file " + filePath);
		manager.onLocalFileHardDelete(filePath);
		sleepMillis(200);
		manager.onLocalFileDeleted(filePath);
		sleepMillis(10);
	}

	/**
	 * This test simulates the the process of creating AND moving/renaming a file
	 * before the upload to the network was triggered. Therefore, the old file should
	 * be ignored (initial state, where execute does nothing) and the new file should
	 * be pushed as a create.
	 */

	@Test
	public void createOnLocalMove(){

		//sleepMillis(ActionExecutor.ACTION_WAIT_TIME_MS*3);
		long start = System.currentTimeMillis();

		BlockingQueue<FileComponent> actionsToCheck = manager.getFileComponentQueue().getQueue();;
		assertTrue(actionsToCheck.size() == 0);

		manager.onLocalFileCreated(Paths.get(filePaths.get(4)));

		sleepMillis(10);

		//move the file LOCALLY

		Paths.get(filePaths.get(4)).toFile().delete();
		manager.onLocalFileDeleted(Paths.get(filePaths.get(4)));
		sleepMillis(10);

		manager.onLocalFileCreated(Paths.get(filePaths.get(5)));
		//sleepMillis(10);

		FileComponent head = actionsToCheck.peek();
		System.out.println("actionsToCheck.size(): " + actionsToCheck.size());
		ArrayList<FileComponent> array = new ArrayList<FileComponent>(actionsToCheck);
		for(FileComponent comp : array){
			System.out.println(comp.getPath() + ": " + comp.getAction().getCurrentState().getClass().toString());
		}
		assertTrue(actionsToCheck.size() == 2);
		assertTrue(array.get(0).getAction().getCurrentState() instanceof InitialState);
		assertTrue(array.get(0).getPath().toString().equals(filePaths.get(4)));
		assertTrue(array.get(1).getAction().getCurrentState() instanceof LocalCreateState);
		assertTrue(array.get(1).getPath().toString().equals(filePaths.get(5)));

		long end = System.currentTimeMillis();
		assertTrue(end - start <= config.getAggregationIntervalInMillis());
		sleepMillis(config.getAggregationIntervalInMillis() * 5);
	}

	/**
	 * Wait the defined time interval. Useful to guarantee different timestamps in
	 * milliseconds if events are programatically created. Furthermore allows to wait
	 * for a cleaned action queue if ActionExecutor.ACTION_TIME_TO_WAIT * 2 is passed
	 * as millisToSleep
	 */
	public static void sleepMillis(long millisToSleep){
		try {
			Thread.sleep(millisToSleep);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}