/*
 * JBoss, Home of Professional Open Source
 * Copyright 2014, JBoss Inc., and individual contributors as indicated
 * by the @authors tag.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jboss.as.patching.management;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.File;
import java.io.FileFilter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
 * Tests "safe" delete operation {@link DeleteOp}
 *
 * @author Bartosz Spyrko-Smietanko
 */
public class DeleteOpTest {
    public static final boolean NOT_PROTECTED = false;
    public static final boolean PROTECTED = true;
    public static final FilenameFilter INCLUDE_ALL = (d,f) -> true;
    private static String TEST_DIR = "test";
    public static final String BACKUP_ROOT_FILE_NAME = TEST_DIR + "/" + ".bkp";

    @Before
    public void setUp() {
        final File testDir = new File(TEST_DIR);
        if (testDir.exists()) {
            recursiveCleanUp(testDir);
        }

        testDir.mkdir();
    }

    @After
    public void tearDown() {
        final File testDir = new File(TEST_DIR);
        if (testDir.exists()) {
            recursiveCleanUp(testDir);
        }
    }

    private void recursiveCleanUp(File file) {
        if (file.isDirectory()) {
            for (File child : file.listFiles()) {
                recursiveCleanUp(child);
            }
        }
        file.delete();
    }

    @Test
    public void removeNonProtectedFileWithoutErrors() throws Exception {
        final File origin = createFile("test.txt", NOT_PROTECTED);

        delete(origin, INCLUDE_ALL);

        assertOriginalFileRemoved("test.txt");
    }

    @Test
    public void failToDeleteProtectedFile() throws Exception {
        final File origin = createFile("test.txt", PROTECTED);

        delete(origin, INCLUDE_ALL);

        assertOriginalFileExists("test.txt");
    }

    @Test
    public void removeFolderWithUnprotectedFiles() throws Exception {
        File dir = createDir("parent", createFile("parent/test.txt", NOT_PROTECTED));

        delete(dir, INCLUDE_ALL);

        assertOriginalFileRemoved("parent");
        assertBackupFolderWasRemoved();
    }

    @Test
    public void failToRemoveParentIfChildIsProtected() throws Exception {
        File dir = createDir("parent", createFile("parent/test.txt", PROTECTED));

        delete(dir, INCLUDE_ALL);

        assertOriginalFileExists("parent");
        assertOriginalFileExists("parent/test.txt");
        assertBackupFolderWasRemoved();
    }

    @Test
    public void doNotDeleteAnyFilesIfOneFileIsProtected() throws Exception {
        File dir = createDir("parent", createFile("parent/test2.txt", NOT_PROTECTED), createFile("parent/test.txt", PROTECTED));

        delete(dir, INCLUDE_ALL);

        assertOriginalFileExists("parent");
        assertOriginalFileExists("parent/test.txt");
        assertOriginalFileExists("parent/test2.txt");
        assertBackupFolderWasRemoved();
    }

    @Test
    public void executeTwoDeleteOpsWithSameBackupDir() throws Exception {
        final File dir1 = createDir("dir1", createFile("dir1/test.txt", NOT_PROTECTED));
        final File dir2 = createDir("dir2", createFile("dir2/test.txt", PROTECTED));

        final DeleteOp op1 = new DeleteOp(dir1, INCLUDE_ALL);
        final DeleteOp op2 = new DeleteOp(dir2, INCLUDE_ALL);
        try {
            DeleteOp.execute(Arrays.asList(op1, op2));

            fail("DeleteOp should throw an exception");
        } catch(IOException e) {
            assertOriginalFileExists("dir1/test.txt");
            assertOriginalFileExists("dir2/test.txt");
        }
    }

    @Test
    public void dontRemoveIgnoredFiles() throws Exception {
        final File dir = createDir("dir1", createFile("dir1/test1.txt", NOT_PROTECTED), createFile("dir1/test2.txt", NOT_PROTECTED));

        boolean operationResult = delete(dir, exclude("test1.txt"));

        assertTrue("The delete with ignored files should be succesful", operationResult);
        assertOriginalFileExists("dir1/test1.txt");
        assertOriginalFileRemoved("dir1/test2.txt");
    }

    @Test
    public void dontRemoveIgnoredDirectories() throws Exception {
        final File dir = createDir("dir1",
                            createDir("dir2",
                                    createFile("dir1/dir2/test1.txt", NOT_PROTECTED)),
                            createFile("dir1/test2.txt", NOT_PROTECTED));

        delete(dir, exclude("dir2"));

        assertOriginalFileExists("dir1/dir2/test1.txt");
        assertOriginalFileRemoved("dir1/test2.txt");
    }

    @Test
    public void completeSuccesfullyIfAllFilesWereIgnored() throws Exception {
        final File dir = createDir("dir1");

        final DeleteOp op1 = new DeleteOp(dir, (d,f)->false); // exclude all files
        DeleteOp.execute(Arrays.asList(op1));

        assertOriginalFileExists("dir1");
        assertBackupFolderWasRemoved();
    }

    @Test
    public void removeBackupFolderAfterCommit() throws Exception {
        file("dir1").mkdir();
        file("dir1", "test1.txt").createNewFile();

        boolean operationResult = delete(file("dir1"), INCLUDE_ALL);

        assertTrue("The delete should have succeeded", operationResult);
        assertBackupFolderWasRemoved();
    }

    @Test
    public void removeBackupFolderAfterRollback() throws Exception {
        final File dir1 = createDir("dir1", createFile("dir1/test.txt", PROTECTED));

        boolean operationResult = delete(dir1, INCLUDE_ALL);

        assertFalse("The delete should have failed", operationResult);
        assertBackupFolderWasRemoved();
    }

    @Test
    public void failDeleteIfDeletedFileExistsInBackupFolder() throws Exception {
        createDir(".bkp", createDir("dir1", createFile(".bkp/dir1/test.txt", NOT_PROTECTED)));
        final File dir1 = createDir("dir1", createFile("dir1/test.txt", NOT_PROTECTED));

        boolean operationResult = delete(dir1, INCLUDE_ALL);

        assertFalse("The delete should have failed", operationResult);
    }

    private static void assertOriginalFileExists(String path) {
        final String fullPath = TEST_DIR + "/" + path;
        final File file = new File(fullPath);

        assertTrue("Original file [" + fullPath +  "] should not be removed", file.exists());
    }

    private static void assertOriginalFileRemoved(String path) {
        final String fullPath = TEST_DIR + "/" + path;
        final File file = new File(fullPath);

        assertFalse("Original file [" + fullPath +  "] should be removed", file.exists());
    }

    private static void assertBackupFolderWasRemoved() {
        final File file = new File(BACKUP_ROOT_FILE_NAME);

        assertFalse("Backup folder [" + BACKUP_ROOT_FILE_NAME  + "] should be removed", file.exists());
    }

    private boolean delete(File origin, FilenameFilter filter) throws IOException {
        final DeleteOp op = new DeleteOp(origin, filter);
        try {
            DeleteOp.execute(Arrays.asList(op));
            return true;
        } catch (IOException e) {
            return false;
        }
    }

    private static File file(String... parts) {
        StringBuilder sb = new StringBuilder(TEST_DIR);
        for (String part : parts) {
            sb.append("/").append(part);
        }
        return new File(sb.toString());
    }

    private static FilenameFilter exclude(String fileName) {
        return (d,f) -> !f.equals(fileName);
    }

    private static File createDir(String name, File... files) {
        String fileName = TEST_DIR + "/" + name;
        File dir = new MockFile(fileName, false, files);
        if (!dir.exists()) {
            dir.mkdir();
        }
        return dir;
    }

    private static File createFile(String name, boolean deleteProtected) throws IOException {
        createParents(name);

        String fileName = TEST_DIR + "/" + name;
        File origin = new MockFile(fileName, deleteProtected);
        origin.createNewFile();
        return origin;
    }

    private static void createParents(String name) {
        File parent = new File(TEST_DIR);
        final String[] split = name.split("/");
        for (int i = 0; i < split.length - 1; i++) {
            String p = split[i];
            parent = new File(parent, p);
            parent.mkdir();
        }
    }

    /*
    Since exact behaviour of File depends on the underlying platform, File.setReadonly / File.setWritable does not
    guarantee move protection. MockFile is to help simulate those issues.
     */
    private static class MockFile extends File {
        private File[] children;
        private boolean protect;

        MockFile(String name, boolean protect) {
            super(name);
            this.protect = protect;
        }

        MockFile(String name, boolean protect, File... children) {
            this(name, protect);
            this.children = children;
        }

        @Override
        public boolean delete() {
            if (protect) {
                return false;
            } else {
                return super.delete();
            }
        }

        @Override
        public boolean renameTo(File dest) {
            if (protect) {
                return false;
            } else {
                return super.renameTo(dest);
            }
        }

        @Override
        public File[] listFiles(FileFilter filter) {
            final File[] files = super.listFiles(filter);

            // need to return the mock ones, but skip ones that were removed
            ArrayList<File> filtered = new ArrayList<>();
            for (File file : files) {
                for (File child : children) {
                    if (file.getAbsolutePath().equals(child.getAbsolutePath())) {
                        filtered.add(child);
                    }
                }
            }

            return filtered.toArray(new File[]{});
        }

        @Override
        public File[] listFiles(FilenameFilter filter) {
            final File[] files = super.listFiles(filter);

            // need to return the mock ones, but skip ones that were removed
            ArrayList<File> filtered = new ArrayList<>();
            for (File file : files) {
                for (File child : children) {
                    if (file.getAbsolutePath().equals(child.getAbsolutePath())) {
                        filtered.add(child);
                    }
                }
            }

            return filtered.toArray(new File[]{});
        }
    }
}
