package cloudsync;


import cloudsync.exceptions.CloudsyncException;
import cloudsync.exceptions.FileIOException;
import cloudsync.exceptions.InfoException;
import cloudsync.exceptions.UsageException;
import cloudsync.helper.CmdOptions;
import cloudsync.helper.Crypt;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import org.apache.commons.cli.ParseException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.*;


public class RemoteLocalFilesystemTest {
    
    public RemoteLocalFilesystemTest() {
    }
    
    @BeforeClass
    public static void setUpClass() {
    }
    
    @AfterClass
    public static void tearDownClass() {
    }
    
    @Before
    public void setUp() throws IOException {
    }
    
    @After
    public void tearDown() throws IOException {
    }

    
    
    /**
     * Generate a random hierarchy, backup then restore it. 
     * Check for equality between the origin hierarchy and restored one.
     */
    @Test
    public void test1() throws IOException, ParseException {
        File rootFolder = createRandomHierarchie();
        
        File targetLocalRemoteFolder = Files.createTempDirectory("targetRemoteFolder").toFile();
        File restoreFolder = new File(rootFolder.getParent(),rootFolder.getName()+"_restore");
        File configFile = Files.createTempFile("test1Config", ".config").toFile();

        String targetLocalRemoteFolderPath = FilesystemHelper.fixPathSeparators(targetLocalRemoteFolder.getAbsolutePath());

        String config = "REMOTE_CONNECTOR=LocalFilesystem";
        config += "\n"+ "PASSPHRASE=1234567";
        config += "\n"+ "TARGET_DIR=" + targetLocalRemoteFolderPath;
        config += "\n"+ "CACHEFILE=" + targetLocalRemoteFolderPath + File.separator + ".cloudsync.cache";
        config += "\n"+ "LOGFILE=" + targetLocalRemoteFolderPath + File.separator + ".cloudsync.log";
        Files.write(configFile.toPath(), config.getBytes(), StandardOpenOption.CREATE);
                
        System.out.println("RootFolder: " + rootFolder.getAbsolutePath());
        System.out.println("TargetFolder: " + targetLocalRemoteFolder.getAbsolutePath());

        
        String[] args = new String[]{
            "--nocache",
            "--backup",
            rootFolder.getAbsolutePath(),
            "--name",
            "Test1Backup",
            "--config",
            configFile.getAbsolutePath()
        };
        Cloudsync.main(args);
        
        
        restoreFolder.mkdir();
        args = new String[]{
            "--nocache",
            "--restore",
            restoreFolder.getAbsolutePath(),
            "--name",
            "Test1Backup",
            "--config",
            configFile.getAbsolutePath()
        };
        Cloudsync.main(args);
        
        
        assertTrue(hierarchieEquals(rootFolder, restoreFolder));
        FileUtils.deleteDirectory(rootFolder);
    }
    
    
    /**
     * Create a specific hierarchy
     * Modify files
     * Check if file is in history.
     */
    @Test
    public void test2() throws IOException, ParseException, CloudsyncException, FileIOException, UsageException, InfoException {
        File rootFolder = Files.createTempDirectory("srcTest2").toFile();
        File targetLocalRemoteFolder = Files.createTempDirectory("targetRemoteFolder").toFile();
        File restoreFolder = new File(rootFolder.getParent(),rootFolder.getName()+"_restore");
        File configFile = Files.createTempFile("test2Config", ".config").toFile();
        
        System.out.println("Test2");
        System.out.println("RootFolder: " + rootFolder.getAbsolutePath());
        System.out.println("TargetFolder: " + targetLocalRemoteFolder.getAbsolutePath());

        String targetLocalRemoteFolderPath = FilesystemHelper.fixPathSeparators(targetLocalRemoteFolder.getAbsolutePath());

        String config = "REMOTE_CONNECTOR=LocalFilesystem";
        config += "\n"+ "PASSPHRASE=1234567";
        config += "\n"+ "TARGET_DIR=" + targetLocalRemoteFolderPath;
        config += "\n"+ "CACHEFILE=" + targetLocalRemoteFolderPath + File.separator + ".cloudsync.cache";
        config += "\n"+ "LOGFILE=" + targetLocalRemoteFolderPath + File.separator + ".cloudsync.log";
        config += "\n"+ "HISTORY=3";
        Files.write(configFile.toPath(), config.getBytes(), StandardOpenOption.CREATE);
           
        /**
         * rootFolder
         *  /file1
         *  /subfolder1
         *      /file2
         *      /subfolder2
         *          /file3
         */
        File file1 = new File(rootFolder,"file1");
        File subfolder1 = new File(rootFolder,"subfolder1");
        File file2 = new File(subfolder1,"file2");
        File subfolder2 = new File(subfolder1,"subfolder2");
        File file3 = new File(subfolder2,"file3");
        subfolder1.mkdirs();
        subfolder2.mkdirs();
        
        Files.write(file1.toPath(), "file 1 content".getBytes("UTF-8"), StandardOpenOption.CREATE_NEW);
        Files.write(file2.toPath(), "file 2 content".getBytes("UTF-8"), StandardOpenOption.CREATE_NEW);
        Files.write(file3.toPath(), "file 3 content".getBytes("UTF-8"), StandardOpenOption.CREATE_NEW);
        
        String[] args = new String[]{
            "--nocache",
            "--backup",
            rootFolder.getAbsolutePath(),
            "--name",
            "Test2Backup",
            "--config",
            configFile.getAbsolutePath()
        };
        Cloudsync.main(args);

        // Update files content
        Files.write(file1.toPath(), "new file 1 content updated".getBytes("UTF-8"), StandardOpenOption.TRUNCATE_EXISTING);
        Files.write(file2.toPath(), "new file 2 content updated".getBytes("UTF-8"), StandardOpenOption.TRUNCATE_EXISTING);
        Files.write(file3.toPath(), "new file 3 content updated".getBytes("UTF-8"), StandardOpenOption.TRUNCATE_EXISTING);
        Cloudsync.main(args);
        
        // Check if history exist for updated files
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy.MM.dd");
        File historyFolder = new File(targetLocalRemoteFolder,"Test2Backup"+"_history_"+sdf.format(new Date()));
        
        CmdOptions cmdOptions = new CmdOptions(args);
        cmdOptions.parse();
        Crypt crypt = new Crypt(cmdOptions);
        assertEquals(historyFolder.listFiles().length,3);
        for(File f1 : historyFolder.listFiles()) {
            if(f1.getName().endsWith(".metadata")) {
                //TODO
            }
            else if(f1.isFile()) {
                assertEquals(crypt.decryptText(f1.getName()), "file1");
                _assertEncryptedFileContentEquals(f1,crypt,"file 1 content");
            }
            else {
                assertEquals(crypt.decryptText(f1.getName()), "subfolder1");
                assertEquals(f1.listFiles().length,3);
                for(File f2 : f1.listFiles()) {
                    if(f2.getName().endsWith(".metadata")) {
                        //TODO
                    }
                    else if(f2.isFile()) {
                        assertEquals(crypt.decryptText(f2.getName()), "file2");
                        _assertEncryptedFileContentEquals(f2,crypt,"file 2 content");
                    }
                    else {
                        assertEquals(f2.listFiles().length,2);
                        assertEquals(crypt.decryptText(f2.getName()), "subfolder2");
                        for(File f3 : f2.listFiles()) {
                            if(f3.getName().endsWith(".metadata")) {
                                //TODO
                            }
                            else {
                                assertEquals(crypt.decryptText(f3.getName()), "file3");
                                _assertEncryptedFileContentEquals(f3,crypt,"file 3 content");
                            }
                        }
                    }
                }
            }
        }     
        
        restoreFolder.mkdir();
        args = new String[]{
            "--nocache",
            "--restore",
            restoreFolder.getAbsolutePath(),
            "--name",
            "Test2Backup",
            "--config",
            configFile.getAbsolutePath()
        };
        Cloudsync.main(args);
        
        
        assertTrue(hierarchieEquals(rootFolder, restoreFolder));
        FileUtils.deleteDirectory(rootFolder);
    }
   
    private void _assertEncryptedFileContentEquals(File file, Crypt crypt, String content) throws IOException, CloudsyncException {
        byte[] encryptedFileContent = Files.readAllBytes(file.toPath());
        ByteArrayInputStream bais = new ByteArrayInputStream(encryptedFileContent);
        byte[] fileContent = IOUtils.toByteArray(crypt.decryptData(bais));
        String s = new String(fileContent, "UTF-8");
        assertEquals(s,content);
    }
    
    
    public static File createRandomHierarchie() throws IOException {
        Random random = new Random();

        int prefixLength = random.nextInt(10)+1;
        String prefix = "src"+org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric(prefixLength);

        File root = Files.createTempDirectory(prefix).toFile();
        root.mkdirs();
        createRandomHierarchie(root,(random.nextDouble()+1.)*3.);
        return root;
    }

    public static void createRandomHierarchie(File parent, double nbFolderProba) throws IOException {
        Random random = new Random();

        double r = random.nextDouble();
        while(r < nbFolderProba) {
            nbFolderProba = nbFolderProba / (random.nextDouble() + 1.);
            File folder = createRandomFolder(parent,random.nextInt(20));
            createRandomHierarchie(folder,nbFolderProba);
            r = random.nextDouble();
        }
    }
    
    
    
    public static void createRandomFile(File inFolder) throws IOException {
        Random random = new Random();
        
        int prefixLength = random.nextInt(10)+3;
        String prefix = org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric(prefixLength);
        String suffix = org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric(3);
        File f = File.createTempFile(prefix, suffix, inFolder);
        
        
        int n = random.nextInt(50);
        while(n != 0) {
            int l = random.nextInt(1000);
            byte[] data = new byte[l];
            random.nextBytes(data);
            Files.write(f.toPath(), data, StandardOpenOption.CREATE,StandardOpenOption.APPEND);
            n = n - 1;
        }
    }

    
    public static File createRandomFolder(File inFolder, int nbFile) throws IOException {
        Random random = new Random();
        int prefixLength = random.nextInt(10)+1;
        String prefix = org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric(prefixLength);
        File result = Files.createTempDirectory(inFolder.toPath(), prefix).toFile();
        result.mkdirs();
        for(int i = 0;i<nbFile;i++) {
            createRandomFile(result);
        }
        return result;
    }
    
    
    public static boolean hierarchieEquals(File root1,File root2) throws IOException {
        File[] files1 = root1.listFiles();
        File[] files2 = root2.listFiles();
        if(files1.length != files2.length) {
            return false;
        }
        
        Map<String,File> mapFiles2 = new HashMap<>();
        for(File f : files2) {
            mapFiles2.put(f.getName(), f);
        }
        
        for(File f1 : files1) {
            if(!mapFiles2.containsKey(f1.getName())) {
                return false;
            }
            
            File f2 = mapFiles2.get(f1.getName());
            if(f1.isDirectory() && f2.isDirectory()) {
                if(!hierarchieEquals(f1,f2)) {
                    return false;
                }
            }
            else if(f1.isFile() && f2.isFile()) {
                if(!FileUtils.contentEquals(f1, f2)) {
                    return false;
                }
            }
            else {
                return false;
            }
            
            mapFiles2.remove(f1.getName());
        }
        
        if(!mapFiles2.isEmpty()) {
            return false;
        }
        
        return true;
    }
    
}