package org.xbib.files.ftp.fs.test;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.mockito.Mockito;
import org.xbib.files.ftp.mock.fake.FakeFtpServer;
import org.xbib.files.ftp.mock.fake.UserAccount;
import org.xbib.files.ftp.mock.fake.filesystem.DirectoryEntry;
import org.xbib.files.ftp.mock.fake.filesystem.FileEntry;
import org.xbib.files.ftp.mock.fake.filesystem.FileSystem;
import org.xbib.files.ftp.mock.fake.filesystem.FileSystemEntry;
import org.xbib.files.ftp.mock.fake.filesystem.UnixFakeFileSystem;
import org.xbib.files.ftp.fs.DefaultFileSystemExceptionFactory;
import org.xbib.files.ftp.fs.FTPEnvironment;
import org.xbib.files.ftp.fs.FTPFileSystem;
import org.xbib.files.ftp.fs.FTPFileSystemProvider;
import org.xbib.files.ftp.fs.FTPPath;
import org.xbib.files.ftp.fs.FileSystemExceptionFactory;
import org.xbib.files.ftp.fs.test.server.ExtendedUnixFakeFileSystem;
import org.xbib.files.ftp.fs.test.server.ListHiddenFilesCommandHandler;
import org.xbib.files.ftp.fs.test.server.MDTMCommandHandler;
import org.xbib.files.ftp.fs.test.server.SymbolicLinkEntry;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystemException;
import java.nio.file.OpenOption;
import java.util.Collection;
import java.util.Map;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;

public abstract class AbstractFTPFileSystemTest {

    private static final String USERNAME = "TEST_USER";
    private static final String PASSWORD = "TEST_PASSWORD";
    private static final String HOME_DIR = "/home/test";

    private static FakeFtpServer unixFtpServer;
    private static ExceptionFactoryWrapper exceptionFactory;
    private static FTPFileSystem unixFtpFileSystem;
    private static FTPFileSystem multiClientUnixFtpFileSystem;
    private FileSystem fileSystem;

    @BeforeAll
    public static void setupClass() throws IOException {
        unixFtpServer = new FakeFtpServer();
        unixFtpServer.setSystemName("UNIX");
        unixFtpServer.setServerControlPort(0);
        FileSystem initFileSystem = new UnixFakeFileSystem();
        initFileSystem.add(new DirectoryEntry(HOME_DIR));
        unixFtpServer.setFileSystem(initFileSystem);
        UserAccount userAccount = new UserAccount(USERNAME, PASSWORD, HOME_DIR);
        unixFtpServer.addUserAccount(userAccount);
        unixFtpServer.setCommandHandler("LIST", new ListHiddenFilesCommandHandler(true));
        unixFtpServer.setCommandHandler("MDTM", new MDTMCommandHandler());
        unixFtpServer.start();
        exceptionFactory = new ExceptionFactoryWrapper();
        unixFtpFileSystem = createFileSystem(unixFtpServer.getServerControlPort());
        multiClientUnixFtpFileSystem = createFileSystem(unixFtpServer.getServerControlPort(), 3);
    }

    @AfterAll
    public static void cleanupClass() throws IOException {
        unixFtpFileSystem.close();
        multiClientUnixFtpFileSystem.close();
        unixFtpServer.stop();
        unixFtpServer = null;
    }

    private static FTPFileSystem createFileSystem(int port) throws IOException {
        Map<String, ?> env = createEnv();
        return (FTPFileSystem) new FTPFileSystemProvider().newFileSystem(URI.create("ftp://localhost:" + port), env);
    }

    private static FTPFileSystem createFileSystem(int port, int clientConnectionCount) throws IOException {
        Map<String, ?> env = createEnv().withClientConnectionCount(clientConnectionCount);
        return (FTPFileSystem) new FTPFileSystemProvider().newFileSystem(URI.create("ftp://localhost:" + port), env);
    }

    protected static FTPEnvironment createEnv() {
        return new FTPEnvironment()
                .withCredentials(USERNAME, PASSWORD)
                .withClientConnectionCount(1)
                .withFileSystemExceptionFactory(exceptionFactory);
    }

    @BeforeEach
    public void setup() {
        fileSystem = new ExtendedUnixFakeFileSystem();
        fileSystem.add(new DirectoryEntry(HOME_DIR));
        unixFtpServer.setFileSystem(fileSystem);
        exceptionFactory.delegate = Mockito.spy(DefaultFileSystemExceptionFactory.INSTANCE);
    }

    @AfterEach
    public void cleanup() {
        exceptionFactory.delegate = null;
        unixFtpServer.setFileSystem(null);
        fileSystem = null;
    }

    protected final String getBaseUrl() {
        return "ftp://" + USERNAME + "@localhost:" + unixFtpServer.getServerControlPort();
    }

    protected final URI getURI() {
        return URI.create("ftp://localhost:" + unixFtpServer.getServerControlPort());
    }

    protected final FTPPath createPath(String path) {
        return new FTPPath(getFileSystem(), path);
    }

    protected final FTPPath createPath(FTPFileSystem fs, String path) {
        return new FTPPath(fs, path);
    }

    protected final FTPFileSystem getFileSystem() {
        return unixFtpFileSystem;
    }

    protected final FTPFileSystem getMultiClientFileSystem() {
        return  multiClientUnixFtpFileSystem;
    }

    protected final FileSystemExceptionFactory getExceptionFactory() {
        return exceptionFactory.delegate;
    }

    protected final FileSystemEntry getFileSystemEntry(String path) {
        return fileSystem.getEntry(path);
    }

    protected final FileEntry getFile(String path) {
        return getFileSystemEntry(path, FileEntry.class);
    }

    protected final DirectoryEntry getDirectory(String path) {
        return getFileSystemEntry(path, DirectoryEntry.class);
    }

    protected final SymbolicLinkEntry getSymLink(String path) {
        return getFileSystemEntry(path, SymbolicLinkEntry.class);
    }

    protected final <T extends FileSystemEntry> T getFileSystemEntry(String path, Class<T> cls) {
        FileSystemEntry entry = getFileSystemEntry(path);
        assertThat(entry, instanceOf(cls));
        return cls.cast(entry);
    }

    protected final FileEntry addFile(String path) {
        FileEntry file = new FileEntry(path);
        fileSystem.add(file);
        return file;
    }

    protected final DirectoryEntry addDirectory(String path) {
        DirectoryEntry directory = new DirectoryEntry(path);
        fileSystem.add(directory);
        return directory;
    }

    protected final SymbolicLinkEntry addSymLink(String path, FileSystemEntry target) {
        SymbolicLinkEntry symLink = new SymbolicLinkEntry(path, target);
        fileSystem.add(symLink);
        return symLink;
    }

    protected final boolean delete(String path) {
        return fileSystem.delete(path);
    }

    protected final int getChildCount(String path) {
        return fileSystem.listFiles(path).size();
    }

    protected final byte[] getContents(FileEntry file) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try (InputStream in = file.createInputStream()) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = in.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
        }
        return out.toByteArray();
    }

    protected final String getStringContents(FileEntry file) throws IOException {
        StringBuilder sb = new StringBuilder((int) file.getSize());
        try (Reader in = new InputStreamReader(file.createInputStream(), StandardCharsets.UTF_8)) {
            char[] buffer = new char[1024];
            int len;
            while ((len = in.read(buffer)) != -1) {
                sb.append(buffer, 0, len);
            }
        }
        return sb.toString();
    }

    protected final long getTotalSize() {
        return getTotalSize(fileSystem.getEntry("/"));
    }

    private long getTotalSize(FileSystemEntry entry) {
        long size = entry.getSize();
        if (entry instanceof DirectoryEntry) {
            for (Object o : fileSystem.listFiles(entry.getPath())) {
                size += getTotalSize((FileSystemEntry) o);
            }
        }
        return size;
    }

    private static class ExceptionFactoryWrapper implements FileSystemExceptionFactory {

        private FileSystemExceptionFactory delegate;

        @Override
        public FileSystemException createGetFileException(String file, int replyCode, String replyString) {
            return delegate.createGetFileException(file, replyCode, replyString);
        }

        @Override
        public FileSystemException createChangeWorkingDirectoryException(String directory, int replyCode, String replyString) {
            return delegate.createChangeWorkingDirectoryException(directory, replyCode, replyString);
        }

        @Override
        public FileAlreadyExistsException createCreateDirectoryException(String directory, int replyCode, String replyString) {
            return delegate.createCreateDirectoryException(directory, replyCode, replyString);
        }

        @Override
        public FileSystemException createDeleteException(String file, int replyCode, String replyString, boolean isDirectory) {
            return delegate.createDeleteException(file, replyCode, replyString, isDirectory);
        }

        @Override
        public FileSystemException createNewInputStreamException(String file, int replyCode, String replyString) {
            return delegate.createNewInputStreamException(file, replyCode, replyString);
        }

        @Override
        public FileSystemException createNewOutputStreamException(String file, int replyCode, String replyString,
                                                                  Collection<? extends OpenOption> options) {
            return delegate.createNewOutputStreamException(file, replyCode, replyString, options);
        }

        @Override
        public FileSystemException createCopyException(String file, String other, int replyCode, String replyString) {
            return delegate.createCopyException(file, other, replyCode, replyString);
        }

        @Override
        public FileSystemException createMoveException(String file, String other, int replyCode, String replyString) {
            return delegate.createMoveException(file, other, replyCode, replyString);
        }
    }
}
