package com.pastdev.jsch.nio.file;


import static com.pastdev.jsch.nio.file.UnixSshFileSystemProvider.PATH_SEPARATOR;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;


import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.DirectoryStream;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileTime;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.UUID;


import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


import com.pastdev.jsch.IOUtils;


public class UnixSshFileSystemTest extends FileSystemTestUtils {
    private static Logger logger = LoggerFactory.getLogger( UnixSshFileSystemTest.class );
    private static final String expected = "Lets give em something to talk about.";

    @AfterClass
    public static void afterClass() {
        closeFileSystem();
    }

    @BeforeClass
    public static void beforeClass() {
        initializeFileSystem( UnixSshFileSystemProvider.SCHEME_SSH_UNIX );
    }

    @Test
    public void testCreateDirectory() {
        String root = UUID.randomUUID().toString();
        String dir = "testcreatdir";

        File rootDir = new File( filesystemPath, root );
        Path path = FileSystems.getFileSystem( uri ).getPath( root ).resolve( dir );
        try {
            logger.debug( "making root dir {}", rootDir );
            rootDir.mkdirs();

            logger.trace( "creating subdirectory {}", path );
            path.getFileSystem().provider().createDirectory( path );
            logger.trace( "created" );

            assertTrue( Files.isDirectory( Paths.get( rootDir.getAbsolutePath(), dir ) ) );
        }
        catch ( IOException e ) {
            logger.error( "failed for {}: {}", path, e );
            logger.debug( "failed:", e );
            fail( "failed for " + path + ": " + e.getMessage() );
        }
        finally {
            logger.trace( "deleting [{}]", rootDir );
            IOUtils.deleteFiles( new File( rootDir, dir ), rootDir );
        }
    }

    @Test
    public void testDirectoryStreamEmptyDir() throws IOException {
        final String root = UUID.randomUUID().toString();
        Path rootPath = Paths.get( filesystemPath, root );

        // create test dir
        assertFalse( Files.exists( rootPath ) );
        Files.createDirectories( rootPath );
        assertTrue( Files.exists( rootPath ) );
        assertTrue( Files.isDirectory( rootPath ) );

        // test dirstream
        DirectoryStream<Path> ds = Files.newDirectoryStream(
                FileSystems.getFileSystem( uri ).getPath( root ) );
        try {
            Iterator<Path> iter = ds.iterator();
            assertFalse( iter.hasNext() );
            try {
                iter.next();
                fail( "expected an exception" );
            }
            catch ( NoSuchElementException e ) {
                // pass
            }
        }
        finally {
            ds.close();
        }
    }

    @Test
    public void testExists() {
        Path defaultPath = Paths.get( uri );
        assertTrue( Files.exists( defaultPath ) );
        assertTrue( Files.isDirectory( defaultPath ) );
        assertEquals( sshPath, defaultPath.toString() );
        Path rootPath = defaultPath.resolve( "/" );
        assertTrue( Files.exists( rootPath ) );
        assertTrue( Files.isDirectory( defaultPath ) );
        assertEquals( "/", rootPath.toString() );
    }

    @Test
    public void testGetPath() {
        FileSystem fileSystem = FileSystems.getFileSystem( uri );
        assertTrue( UnixSshFileSystem.class.isAssignableFrom( fileSystem.getClass() ) );
        assertEquals( "/", fileSystem.getPath( "/" ).toString() );
        assertEquals( "/", fileSystem.getPath( "/" ).toAbsolutePath().toString() );
        assertEquals( "aaa", fileSystem.getPath( "aaa" ).toString() );
        assertEquals( sshPath + "/aaa", fileSystem.getPath( "aaa" ).toAbsolutePath().toString() );
        assertEquals( "/aaa", fileSystem.getPath( "/aaa" ).toString() );
        assertEquals( "/aaa", fileSystem.getPath( "/aaa" ).toAbsolutePath().toString() );
    }

    @Test
    public void testNewDirectoryStream() {
        final String root = UUID.randomUUID().toString();
        final String filename1 = "silly1.txt";
        final String filename2 = "silly2.txt";
        final String filename3 = "silly3.txt";
        final String filename4 = "silly4.txt";
        final String dotfilename = ".sillly.txt";

        File rootDir = new File( filesystemPath, root );
        File file1 = new File( rootDir, filename1 );
        File file2 = new File( rootDir, filename2 );
        File file3 = new File( rootDir, filename3 );
        File file4 = new File( rootDir, filename4 );
        File dotfile = new File( rootDir, dotfilename );
        try {
            rootDir.mkdirs();
            IOUtils.writeFile( file1, expected, UTF8 );
            IOUtils.writeFile( file2, expected, UTF8 );
            IOUtils.writeFile( file3, expected, UTF8 );
            IOUtils.writeFile( file4, expected, UTF8 );
            IOUtils.writeFile( dotfile, expected, UTF8 );
        }
        catch ( IOException e ) {
            logger.error( "could not write files to {}: {}", rootDir, e );
            logger.debug( "could not write to file:", e );
            fail( "could not write files to " + rootDir + ": " + e.getMessage() );
        }

        Path rootPath = FileSystems.getFileSystem( uri ).getPath( root );
        Set<String> expectedEntries = new HashSet<String>();
        expectedEntries.add( rootPath.resolve( filename1 ).toString() );
        expectedEntries.add( rootPath.resolve( filename2 ).toString() );
        expectedEntries.add( rootPath.resolve( filename3 ).toString() );
        expectedEntries.add( rootPath.resolve( filename4 ).toString() );
        expectedEntries.add( rootPath.resolve( dotfilename ).toString() );

        try {
            DirectoryStream<Path> directoryStream = null;
            try {
                directoryStream = rootPath.getFileSystem().provider().newDirectoryStream( rootPath, new Filter<Path>() {
                    @Override
                    public boolean accept( Path path ) throws IOException {
                        if ( path.getFileName().toString().equals( filename1 ) ) {
                            return false;
                        }
                        else {
                            return true;
                        }
                    }
                } );
                for ( Path directoryEntry : directoryStream ) {
                    assertTrue( expectedEntries.remove( directoryEntry.toString() ) );
                }
                assertTrue( expectedEntries.remove( rootPath.resolve( filename1 ).toString() ) );
                assertTrue( expectedEntries.isEmpty() );
            }
            finally {
                IOUtils.closeAndLogException( directoryStream );
            }
        }
        catch ( IOException e ) {
            logger.error( "could not obtain directory stream from {}: {}", rootPath, e );
            logger.debug( "could not obtain directory stream:", e );
            fail( "could not obtain directory stream from " + rootPath + ": " + e.getMessage() );
        }
        finally {
            IOUtils.deleteFiles( file1, file1, file3, file4, dotfile, rootDir );
        }
    }

    @Test
    public void testNewInputStream() {
        String root = UUID.randomUUID().toString();
        String filename = "outputstreamtest.txt";

        File rootDir = new File( filesystemPath, root );
        File file = new File( rootDir, filename );
        Path filePath = FileSystems.getFileSystem( uri ).getPath( root ).resolve( filename );
        try {
            rootDir.mkdirs();

            IOUtils.writeFile( file, expected );

            InputStream inputStream = null;
            try {
                inputStream = filePath.getFileSystem().provider().newInputStream( filePath );
                assertEquals( expected, IOUtils.copyToString( inputStream ) );
            }
            finally {
                IOUtils.closeAndLogException( inputStream );
            }
        }
        catch ( IOException e ) {
            logger.error( "failed for {}: {}", filePath, e );
            logger.debug( "failed:", e );
            fail( "failed for " + filePath + ": " + e.getMessage() );
        }
        finally {
            IOUtils.deleteFiles( file, rootDir );
        }
    }

    @Test
    public void testNewOutputStream() {
        String root = UUID.randomUUID().toString();
        String filename = "outputstreamtest.txt";

        File rootDir = new File( filesystemPath, root );
        File file = new File( rootDir, filename );
        Path path = FileSystems.getFileSystem( uri ).getPath( root ).resolve( filename );
        try {
            logger.debug( "making dir {}", rootDir );
            rootDir.mkdirs();

            OutputStream outputStream = null;
            try {
                logger.trace( "getting outputstream" );
                outputStream = path.getFileSystem().provider().newOutputStream( path );
                logger.trace( "writing to outputstream" );
                IOUtils.copyFromString( expected, outputStream );
                logger.trace( "writing complete" );
            }
            finally {
                IOUtils.closeAndLogException( outputStream );
            }

            logger.trace( "checking file contents" );
            assertEquals( expected, IOUtils.readFile( file, UTF8 ) );
            logger.trace( "file contents match" );
        }
        catch ( IOException e ) {
            logger.error( "failed for {}: {}", path, e );
            logger.debug( "failed:", e );
            fail( "failed for " + path + ": " + e.getMessage() );
        }
        finally {
            IOUtils.deleteFiles( file, rootDir );
        }
    }

    @Test
    public void testPosixFileAttributes() {
        String root = UUID.randomUUID().toString();
        String filename = "silly.txt";

        File rootDir = new File( filesystemPath, root );
        File file = new File( rootDir, filename );
        try {
            rootDir.mkdirs();
            IOUtils.writeFile( file, expected, UTF8 );
        }
        catch ( IOException e ) {
            logger.error( "could not write to {}: {}", file, e );
            logger.debug( "could not write to file:", e );
            fail( "could not write to " + file + ": " + e.getMessage() );
        }

        Path path = FileSystems.getFileSystem( uri ).getPath( root ).resolve( filename );
        try {
            long now = new Date().getTime();
            PosixFileAttributes attributes = path.getFileSystem().provider().readAttributes( path, PosixFileAttributes.class );

            assertTrue( now > attributes.creationTime().toMillis() );
            assertTrue( now > attributes.lastAccessTime().toMillis() );
            assertTrue( now > attributes.lastModifiedTime().toMillis() );
            assertTrue( attributes.isRegularFile() );
            assertFalse( attributes.isDirectory() );
            assertFalse( attributes.isSymbolicLink() );
            assertFalse( attributes.isOther() );
            assertEquals( expected.length(), attributes.size() );
            assertNotNull( attributes.fileKey() );
        }
        catch ( IOException e ) {
            logger.error( "could not read attribues from {}: {}", path, e );
            logger.debug( "could not read attributes:", e );
            fail( "could not read attributes from " + path + ": " + e.getMessage() );
        }
        finally {
            IOUtils.deleteFiles( file, rootDir );
        }
    }

    @Test
    public void testReadAttributes() {
        String root = UUID.randomUUID().toString();
        String filename = "silly.txt";

        File rootDir = new File( filesystemPath, root );
        File file = new File( rootDir, filename );
        try {
            rootDir.mkdirs();
            IOUtils.writeFile( file, expected, UTF8 );
        }
        catch ( IOException e ) {
            logger.error( "could not write to {}: {}", file, e );
            logger.debug( "could not write to file:", e );
            fail( "could not write to " + file + ": " + e.getMessage() );
        }

        Path path = FileSystems.getFileSystem( uri ).getPath( root ).resolve( filename );
        try {
            long now = new Date().getTime();
            Map<String, Object> map = path.getFileSystem().provider().readAttributes( path, "creationTime,size,fileKey" );

            assertTrue( now > ((FileTime) map.get( "creationTime" )).toMillis() );
            assertEquals( Long.valueOf( expected.length() ), (Long) map.get( "size" ) );
            assertNotNull( map.get( "fileKey" ) );
        }
        catch ( IOException e ) {
            logger.error( "could not read attribues from {}: {}", path, e );
            logger.debug( "could not read attributes:", e );
            fail( "could not read attributes from " + path + ": " + e.getMessage() );
        }
        finally {
            IOUtils.deleteFiles( file, rootDir );
        	try {
        		// should add wait to delete files to ensure delete is complete... but...
				Thread.sleep(1000);
			} catch (InterruptedException e) { }
        }

        try {
            // should throw exception as file no longer exists
            path.getFileSystem().provider().readAttributes( path, "creationTime,size,fileKey" );
            fail( "NoSuchFileException should have been thrown" );
        }
        catch ( NoSuchFileException e ) {
            logger.debug( "as expected, [{}] does not exist", e.getFile() );
        }
        catch ( IOException e ) {
            logger.error( "could not read attribues from {}: {}", path, e );
            logger.debug( "could not read attributes:", e );
            fail( "could not read attributes from " + path + ": " + e.getMessage() );
        }

        try {
            // should throw exception as dir no longer exists
            path.getFileSystem().provider().readAttributes(
                    FileSystems.getFileSystem( uri ).getPath( root ),
                    "creationTime,size,fileKey" );
            fail( "NoSuchFileException should have been thrown" );
        }
        catch ( NoSuchFileException e ) {
            logger.debug( "as expected, [{}] does not exist", e.getFile() );
        }
        catch ( IOException e ) {
            logger.error( "could not read attribues from {}: {}", path, e );
            logger.debug( "could not read attributes:", e );
            fail( "could not read attributes from " + path + ": " + e.getMessage() );
        }
    }

    @Test
    public void testRelativize() {
        FileSystem fileSystem = FileSystems.getFileSystem( uri );
        Path path = fileSystem.getPath( "this/is/a/test" );
        Path other = fileSystem.getPath( "this/is/a/test/of/the/emergency/broadcast/system" );
        Path crazy = fileSystem.getPath( "this/is/b/test/of/the" );
        Path expectedRelative = fileSystem.getPath( "of/the/emergency/broadcast/system" );
        Path expectedInverseRelative = fileSystem.getPath( "../../../../.." );
        Path expectedCrazyRelative = fileSystem.getPath( "../../b/test/of/the" );
        Path expectedOtherCrazyRelative = fileSystem.getPath( "../../../../../../../b/test/of/the" );
        assertEquals( expectedRelative, path.relativize( other ) );
        assertEquals( expectedInverseRelative, other.relativize( path ) );
        assertEquals( expectedCrazyRelative, path.relativize( crazy ) );
        assertEquals( expectedOtherCrazyRelative, other.relativize( crazy ) );
    }

    @Test
    public void testSeekableByteChannel() {
        String root = UUID.randomUUID().toString();
        String filename = "outputstreamtest.txt";

        File rootDir = new File( filesystemPath, root );
        File file = new File( rootDir, filename );
        Path filePath = FileSystems.getFileSystem( uri ).getPath( root ).resolve( filename );
        try {
            rootDir.mkdirs();

            IOUtils.writeFile( file, expected, UTF8 );

            try (SeekableByteChannel byteChannel = filePath.getFileSystem().provider().newByteChannel(
                    filePath,
                    EnumSet.of(
                            StandardOpenOption.READ,
                            StandardOpenOption.WRITE ),
                    PosixFilePermissions.asFileAttribute( EnumSet.of(
                            PosixFilePermission.OWNER_READ,
                            PosixFilePermission.OWNER_WRITE,
                            PosixFilePermission.OWNER_EXECUTE ) ) )) {

                byte[] bytes = new byte[4];
                ByteBuffer buffer = ByteBuffer.wrap( bytes );
                byteChannel.position( 3 ).read( buffer );

                String threeToSeven = expected.substring( 3, 7 );
                assertEquals( threeToSeven, new String( bytes, UTF8 ) );

                buffer.position( 0 );
                assertEquals( 4, byteChannel.position( 10 ).write( buffer ) );

                String newExpected = expected.substring( 0, 10 ) + threeToSeven + expected.substring( 14 );
                bytes = new byte[expected.getBytes( UTF8 ).length];
                buffer = ByteBuffer.wrap( bytes );
                byteChannel.position( 0 ).read( buffer );
                assertEquals( newExpected, new String( bytes, UTF8 ) );
            }
        }
        catch ( IOException e ) {
            logger.error( "failed for {}: {}", filePath, e );
            logger.debug( "failed:", e );
            fail( "failed for " + filePath + ": " + e.getMessage() );
        }
        finally {
            IOUtils.deleteFiles( file, rootDir );
        }
    }

    @Test
    public void testStatDirectory() {
        final String root = UUID.randomUUID().toString();
        final String filename1 = "silly1.txt";
        final String filename2 = "silly2.txt";
        final String filename3 = "silly3.txt";
        final String filename4 = "silly4.txt";
        final String dotfilename = ".silly.txt";

        File rootDir = new File( filesystemPath, root );
        File file1 = new File( rootDir, filename1 );
        File file2 = new File( rootDir, filename2 );
        File file3 = new File( rootDir, filename3 );
        File file4 = new File( rootDir, filename4 );
        File dotfile = new File( rootDir, dotfilename );
        try {
            rootDir.mkdirs();
            IOUtils.writeFile( file1, expected, UTF8 );
            IOUtils.writeFile( file2, expected, UTF8 );
            IOUtils.writeFile( file3, expected, UTF8 );
            IOUtils.writeFile( file4, expected, UTF8 );
            IOUtils.writeFile( dotfile, expected, UTF8 );
        }
        catch ( IOException e ) {
            logger.error( "could not write files to {}: {}", rootDir, e );
            logger.debug( "could not write to file:", e );
            fail( "could not write files to " + rootDir + ": " + e.getMessage() );
        }

        UnixSshPath rootPath = (UnixSshPath) FileSystems.getFileSystem( uri ).getPath( root );
        try {
            Map<UnixSshPath, PosixFileAttributes> map = rootPath.getFileSystem().provider().statDirectory( rootPath );
            assertEquals( 5, map.size() );
            assertTrue( map.containsKey( FileSystems.getFileSystem( uri ).getPath( filename1 ) ) );
            assertTrue( map.containsKey( FileSystems.getFileSystem( uri ).getPath( filename2 ) ) );
            assertTrue( map.containsKey( FileSystems.getFileSystem( uri ).getPath( filename3 ) ) );
            assertTrue( map.containsKey( FileSystems.getFileSystem( uri ).getPath( filename4 ) ) );
            assertTrue( map.containsKey( FileSystems.getFileSystem( uri ).getPath( dotfilename ) ) );
        }
        catch ( IOException e ) {
            logger.error( "could not stat directory {}: {}", rootDir, e );
            logger.debug( "could not stat directory:", e );
            fail( "could not stat directory " + rootDir + ": " + e.getMessage() );
        }
    }

    @Test
    public void testUri() {
        String filename = "silly.txt";
        Path tempPath = Paths.get( uri );
        UnixSshPath path = (UnixSshPath) tempPath.resolve( filename );
        assertEquals( username, path.getUsername() );
        assertEquals( hostname, path.getHostname() );
        assertEquals( port, path.getPort() );
        assertEquals( sshPath + PATH_SEPARATOR + filename, path.toString() );
    }
}