// Copyright 2020 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0

package org.terasology.launcher.util;

import org.hamcrest.Matchers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.io.TempDir;
import org.spf4j.log.Level;
import org.spf4j.test.log.TestLoggers;
import org.spf4j.test.matchers.LogMatchers;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.AclEntry;
import java.nio.file.attribute.AclEntryPermission;
import java.nio.file.attribute.AclFileAttributeView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

class TestFileUtils {

    private static final String FILE_NAME = "File";
    private static final String DIRECTORY_NAME = "lorem";
    private static final String SAMPLE_TEXT = "Lorem Ipsum";

    @TempDir
    Path tempFolder;
    boolean isPosix;

    @BeforeEach
    void setup() {
        isPosix = tempFolder.getFileSystem().supportedFileAttributeViews().contains("posix");
    }

    @Test
    @DisabledOnOs(OS.WINDOWS)
    void testCannotCreateDirectory() throws IOException {
        final Path directory = tempFolder.resolve(DIRECTORY_NAME);
        var tempFolderFile = tempFolder.toFile();

        // Unfortunately, Win is not POSIX perms compatible, it uses own ACL system
        List<AclEntry> originalAcl = null;
        AclFileAttributeView view = null;

        if (isPosix) {
            assert tempFolderFile.setWritable(false);
        } else {
            view = Files.getFileAttributeView(directory.getParent(), AclFileAttributeView.class);
            originalAcl = view.getAcl();
            removeAclS(view, false);
        }

        try {
            assertThrows(IOException.class, () ->
                    FileUtils.ensureWritableDir(directory)
            );
        } finally {
            // so @TempDir can tidy up
            if (isPosix) {
                assert tempFolderFile.setWritable(true);
            } else {
                assert view != null;
                view.setAcl(originalAcl);
            }
        }
    }

    @Test
    void testNotDirectory() throws IOException {
        var notDirectory = tempFolder.resolve("notADirectory");
        Files.createFile(notDirectory);

        var exc = assertThrows(IOException.class, () ->
                FileUtils.ensureWritableDir(notDirectory)
        );
        assertThat(exc.getMessage(), startsWith("Not a directory"));
    }

    @Test
    @DisabledOnOs(OS.WINDOWS)
    void testNoPerms() throws IOException {
        var directory = tempFolder.resolve(DIRECTORY_NAME);
        Files.createDirectory(directory);
        var d = directory.toFile();

        // Unfortunately, Win is not POSIX perms compatible, it uses own ACL system
        List<AclEntry> originalAcl = null;
        AclFileAttributeView view = null;

        if (isPosix) {
            assert d.setReadable(false);
            assert d.setWritable(false);
        } else {
            view = Files.getFileAttributeView(directory, AclFileAttributeView.class);
            originalAcl = view.getAcl();
            removeAclS(view, true);
        }

        try {
            var exc = assertThrows(IOException.class, () ->
                    FileUtils.ensureWritableDir(directory)
            );
            assertThat(exc.getMessage(), startsWith("Missing read or write permissions"));
        } finally {
            // oh no, @TempDir doesn't know how to delete this! make it writable again.
            if (isPosix) {
                assert d.setReadable(true);
                assert d.setWritable(true);
            } else {
                assert view != null;
                view.setAcl(originalAcl);
            }
        }
    }

    @Test
    void testDeleteFile() throws IOException {
        Path directory = tempFolder;
        Path file = directory.resolve(FILE_NAME);
        Files.createFile(file);
        assertTrue(Files.exists(file));
        FileUtils.delete(directory);
        assertFalse(Files.exists(file));
        assertFalse(Files.exists(directory));
    }

    @Test
    void testDeleteDirectoryContent() throws IOException {
        Path directory = tempFolder;
        Path file = directory.resolve(FILE_NAME);
        Files.createFile(file);
        assertTrue(Files.exists(file));
        FileUtils.deleteDirectoryContent(directory);
        assertFalse(Files.exists(file));
        assertTrue(Files.exists(directory));
    }

    /**
     * Test that `FileUtils.ensureEmptyDir` creates and empty directory if it does not exist.
     */
    @Test
    void testEnsureEmptyDirCreation() throws IOException {
        Path context = tempFolder;
        // setup
        Path dirToTest = context.resolve(DIRECTORY_NAME);
        assertFalse(Files.exists(dirToTest));
        // test
        FileUtils.ensureEmptyDir(dirToTest);
        assertTrue(Files.exists(dirToTest));
        assertTrue(Files.isDirectory(dirToTest));
        assertEquals(0, Files.list(dirToTest).count());
    }

    /**
     * Test that `FileUtils.ensureEmptyDir` drains (delete all content) if the directory exists.
     */
    @Test
    void testEnsureEmptyDirDrain() throws IOException {
        Path context = tempFolder;
        // setup
        Path dirToTest = context.resolve(DIRECTORY_NAME);
        Path file = dirToTest.resolve(FILE_NAME);
        Files.createDirectory(dirToTest);
        Files.createFile(file);
        // assure directory exists and is not empty
        assertTrue(Files.exists(dirToTest));
        assertTrue(Files.exists(file));
        // test
        FileUtils.ensureEmptyDir(dirToTest);
        assertTrue(Files.exists(dirToTest));
        assertTrue(Files.isDirectory(dirToTest));
        assertEquals(0, Files.list(dirToTest).count());
    }

    @Test
    void testCopyFolder(@TempDir Path source, @TempDir Path destination) throws IOException {
        Path fileInSource = source.resolve(FILE_NAME);
        Files.createFile(fileInSource);
        assertTrue(Files.exists(fileInSource));
        List<String> text = Collections.singletonList(SAMPLE_TEXT);
        Files.write(fileInSource, text, StandardCharsets.UTF_8);

        Path fileInDestination = destination.resolve(FILE_NAME);

        FileUtils.copyFolder(source, destination);

        assertTrue(Files.exists(fileInDestination));
        assertArrayEquals(Files.readAllBytes(fileInSource), Files.readAllBytes(fileInDestination));
    }

    @Test
    void testDeleteFileSilently() throws IOException {
        Path tempFile = tempFolder.resolve(FILE_NAME);
        Files.createFile(tempFile);
        assertTrue(Files.exists(tempFile));

        FileUtils.deleteFileSilently(tempFile);
        assertTrue(Files.notExists(tempFile));
    }

    @Test
    void testDeleteFileSilentlyWithEmptyDirectory() {
        assertTrue(Files.exists(tempFolder));

        FileUtils.deleteFileSilently(tempFolder);
        assertTrue(Files.notExists(tempFolder));
    }

    @Test
    void testDeleteFileSilentlyWithNonEmptyDirectory() throws IOException {
        Path tempFile = tempFolder.resolve(FILE_NAME);
        Files.createFile(tempFile);
        assertTrue(Files.exists(tempFile));

        // DirectoryNotEmptyException will be logged but not thrown
        var loggedException = TestLoggers.sys().expect("", Level.ERROR,
                LogMatchers.hasMatchingExtraThrowable(Matchers.instanceOf(DirectoryNotEmptyException.class))
        );

        FileUtils.deleteFileSilently(tempFolder);

        assertTrue(Files.exists(tempFolder));
        loggedException.assertObservation();
    }

    @Test
    void testExtract(@TempDir Path zipDir, @TempDir Path outputDir) throws IOException {
        final String fileInRoot = "fileInRoot";
        final String fileInFolder = "folder/fileInFolder";
        final String file1Contents = SAMPLE_TEXT + "1";
        final String file2Contents = SAMPLE_TEXT + "2";
        /* An archive with this structure is created:
         * <zip root>
         * +-- fileInRoot
         * +-- folder
         * |   +-- fileInFolder
         */
        Path zipFile = zipDir.resolve(FILE_NAME + ".zip");
        try (ZipOutputStream zipOutputStream = new ZipOutputStream(Files.newOutputStream(zipFile))) {
            zipOutputStream.putNextEntry(new ZipEntry(fileInRoot));
            zipOutputStream.write(file1Contents.getBytes());
            zipOutputStream.closeEntry();
            zipOutputStream.putNextEntry(new ZipEntry(fileInFolder));
            zipOutputStream.write(file2Contents.getBytes());
            zipOutputStream.closeEntry();
        }

        FileUtils.extractZipTo(zipFile, outputDir);
        Path extractedFileInRoot = outputDir.resolve(fileInRoot);
        Path extractedFileInFolder = outputDir.resolve(fileInFolder);
        assertTrue(Files.exists(extractedFileInRoot));
        assertTrue(Files.exists(extractedFileInFolder));
        assertEquals(file1Contents, Files.readAllLines(extractedFileInRoot).get(0));
        assertEquals(file2Contents, Files.readAllLines(extractedFileInFolder).get(0));
    }

    private void removeAclS(AclFileAttributeView view, boolean removeRead) throws IOException {
        var entries = new ArrayList<AclEntry>();
        for (var acl : view.getAcl()) {
            var perms = new LinkedHashSet<>(acl.permissions());
            perms.remove(AclEntryPermission.WRITE_DATA);
            perms.remove(AclEntryPermission.APPEND_DATA);
            perms.remove(AclEntryPermission.ADD_SUBDIRECTORY);
            perms.remove(AclEntryPermission.DELETE);
            perms.remove(AclEntryPermission.DELETE_CHILD);
            if (removeRead) {
                perms.remove(AclEntryPermission.READ_DATA);
            }
            var aclEntry = AclEntry.newBuilder()
                    .setType(acl.type())
                    .setPrincipal(acl.principal())
                    .setPermissions(perms)
                    .setFlags(acl.flags())
                    .build();
            entries.add(aclEntry);
        }
        view.setAcl(entries);
    }

}
