package jnr.posix.windows;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.text.NumberFormat;
import jnr.posix.DummyPOSIXHandler;
import jnr.posix.FileStat;
import jnr.posix.POSIX;
import jnr.posix.POSIXFactory;
import jnr.posix.WindowsPOSIX;
import org.junit.BeforeClass;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

public class WindowsFileTest {
    private static POSIX posix;

    @BeforeClass
    public static void setUpClass() throws Exception {
        posix = POSIXFactory.getPOSIX(new DummyPOSIXHandler(), true);
    }

    private class Pair {
        public File base;
        public File leaf;
        public Pair(File base, File leaf) {
            this.base = base;
            this.leaf = leaf;
        }

        public void cleanup() {
            cleanup(base);
        }

        public void cleanup(File node) {
            if (node.isDirectory()) {
                File[] files = node.listFiles();
                if (files != null) {
                    for(File file: files) {
                        cleanup(file);
                    }
                }
            }
            node.delete();
        }
    }
    // FIXME: This is a broken method since it does not delete any of the generated dirs.
    private static final String DIR_NAME = "0123456789";
    private Pair makeLongPath() throws IOException {
        File tmp = Files.createTempDirectory("temp" + Long.toHexString(System.nanoTime())).toFile();

        StringBuilder buf = new StringBuilder(DIR_NAME);
        for (int i = 0; i < 30; i++) {
            buf.append(DIR_NAME).append('/');
        }
        File tmp2 = new File(tmp, buf.toString());
        tmp2.mkdirs();

        return new Pair(tmp, tmp2);
    }

    @Test
    public void testLowercaseDriveLetter() throws Throwable {
        // FIXME: Not all systems have a C drive but nearly all do
        FileStat st = posix.stat("c:/");
        assertTrue(st.dev() == 2);
        assertTrue(st.rdev() == 2);
    }

    @Test
    public void testFakeUIDGUID() throws Throwable {
        File f = File.createTempFile("stat", null);

        try {
            FileStat st = posix.stat(f.getAbsolutePath());
            assertTrue(st.uid() == 0);
            assertTrue(st.gid() == 0);
        } finally {
            f.delete();
        }
    }

    @Test
    public void testExecutableSuffixesAreExecutable() throws Throwable {
        File f = File.createTempFile("stat", ".exe");

        try {
            FileStat st = posix.stat(f.getAbsolutePath());
            assertEquals("100755", Integer.toOctalString(st.mode()));
            assertTrue(st.isExecutable());
        } finally {
            f.delete();
        }

        f = File.createTempFile("STAT", ".EXE");

        try {
            FileStat st = posix.stat(f.getAbsolutePath());
            assertEquals("100755", Integer.toOctalString(st.mode()));
            assertTrue(st.isExecutable());
        } finally {
            f.delete();
        }

    }

    @Test
    public void testBlocksAndBlockSizeReturn() throws Throwable {
        File f = File.createTempFile("stat", null);

        try {
            FileStat st = posix.stat(f.getAbsolutePath());
            assertTrue(st.blocks() == -1);
            assertTrue(st.blockSize() == -1);
        } finally {
            f.delete();
        }
    }

    @Test
        public void testLongFileRegular() throws Throwable {
        Pair pair = makeLongPath();
        String path = pair.leaf.getAbsolutePath();
        try {
            FileStat st = posix.stat(path);
            assertNotNull("posix.stat failed", st);

            FileStat stat = posix.allocateStat();
            int result = posix.stat(path, stat);
            assertNotNull("posix.stat failed", stat);
            assertEquals(0, result);
        } finally {
            pair.cleanup();
        }
    }

    @Test
    public void testLongFileUNC() throws Throwable {
        Pair pair = makeLongPath();
        String absolutePath = pair.leaf.getAbsolutePath();
        char letter = absolutePath.charAt(0);
        String path = absolutePath.replace(absolutePath.substring(0,2), "\\\\localhost\\" + letter + "$");
        try {
            FileStat st = posix.stat(path);
            assertNotNull("posix.stat failed", st);

            FileStat stat = posix.allocateStat();
            int result = posix.stat(path, stat);
            assertNotNull("posix.stat failed", stat);
            assertEquals(0, result);
        } finally {
            pair.cleanup();
        }
    }

    @Test
    public void statUNCFile() throws Throwable {
        File f = File.createTempFile("stat", null);
        String absolutePath = f.getAbsolutePath();
        char letter = absolutePath.charAt(0);
        String path = absolutePath.replace(absolutePath.substring(0,2), "\\\\localhost\\" + letter + "$");
        try {
            FileStat st = posix.stat(path);
            assertNotNull("posix.stat failed", st);

            FileStat stat = posix.allocateStat();
            int result = posix.stat(path, stat);
            assertNotNull("posix.stat failed", stat);
            assertEquals(0, result);
        } finally {
            f.delete();
        }
    }

    @Test
    public void unlinkTestWindows() throws Throwable {
        File tmp = File.createTempFile("unlinkTest", "tmp");
        RandomAccessFile raf = new RandomAccessFile(tmp, "rw");

        raf.write("hello".getBytes());

        // Windows won't allow you to delete open files, so we must
        // close the handle before trying to delete it.  Unfortunately,
        // this also means we're unable to write to the handle afterwards
        // as we do with the non-Windows test.
        raf.close();

        int res = posix.unlink(tmp.getCanonicalPath());

        assertEquals(0, res);
        assertFalse(tmp.exists());
    }

    @Test
    public void testFindFirstFile() throws Throwable {
        File f = File.createTempFile("stat", null);
        try {

            POSIX posix = POSIXFactory.getNativePOSIX();
            FileStat stat = posix.allocateStat();
            int result = ((WindowsPOSIX) posix).findFirstFile(f.getAbsolutePath(), stat);

            assertEquals(0, result);
            assertTrue(stat.isFile());
        } finally {
            f.delete();
        }
    }

    @Test
    public void testFindFirstFileBogusFile() throws Throwable {
        POSIX posix = POSIXFactory.getNativePOSIX();
        FileStat stat = posix.allocateStat();
        int result = ((WindowsPOSIX) posix).findFirstFile("sdjfhjfsdfhdsdfhsdj", stat);
        assertTrue(result < 0);
    }

    @Test
    public void utimensatWindows() throws Throwable {
        File file = File.createTempFile("utimensat", null);
        try {
            FileStat fileStat = posix.stat(file.getPath());

            long atimeSeconds = fileStat.atime() + 1;
            long mtimeSeconds = fileStat.mtime() - 1;

            // Windows precision is 100 ns
            long atimeNanoSeconds = 123456700;
            long mtimeNanoSeconds = 987654300;

            // dirfd is ignored when passing an absolute path
            // flag can be used to update symlinks
            posix.utimensat(0,
                    file.getAbsolutePath(),
                    new long[]{atimeSeconds, atimeNanoSeconds},
                    new long[]{mtimeSeconds, mtimeNanoSeconds},
                    0);

            fileStat = posix.stat(file.getPath());
            assertEquals("access time should be updated", atimeSeconds, fileStat.atime());
            assertEquals("modification time should be updated", mtimeSeconds, fileStat.mtime());
        } finally {
            file.delete();
        }
    }
    
    @Test
    public void utimensatWindowsCurrentTime() throws Throwable {
        File file = File.createTempFile("file", null);
        try {
            FileStat fileStat = posix.stat(file.getPath());
            long atimeSeconds = fileStat.atime();
            long mtimeSeconds = fileStat.mtime();
            long atimeSecondsInPast = atimeSeconds - 1000;
            long mtimeSecondsInPast = mtimeSeconds - 1000;

            posix.utimensat(0,
                    file.getAbsolutePath(),
                    new long[]{atimeSecondsInPast, 0},
                    new long[]{mtimeSecondsInPast, 0}, 0);
            fileStat = posix.stat(file.getPath());
            assertEquals("access time should be updated", fileStat.atime(), atimeSecondsInPast);
            assertEquals("modification time should be updated", fileStat.mtime(), mtimeSecondsInPast);

            posix.utimensat(0, file.getAbsolutePath(), null, null, 0);

            fileStat = posix.stat(file.getPath());
            assertTrue("access time should be updated to current time",
                    timeWithinRange(fileStat.atime(), atimeSeconds, 10));
            assertTrue("modification time should be updated to current time",
                    timeWithinRange(fileStat.mtime(), mtimeSeconds, 10));
        } finally {
            file.delete();
        }
    }

    @Test
    public void utimenWindowsCurrentTime() throws Throwable {
        File file = File.createTempFile("file", null);
        try {
            FileStat fileStat = posix.stat(file.getPath());
            long atimeSeconds = fileStat.atime();
            long mtimeSeconds = fileStat.mtime();
            long atimeSecondsInPast = atimeSeconds - 1000;
            long mtimeSecondsInPast = mtimeSeconds - 1000;

            posix.utimes(file.getAbsolutePath(),
                    new long[]{atimeSecondsInPast, 0},
                    new long[]{mtimeSecondsInPast, 0});
            fileStat = posix.stat(file.getPath());
            assertEquals("access time should be updated",
                    fileStat.atime(), atimeSecondsInPast);
            assertEquals("modification time should be updated",
                    fileStat.mtime(), mtimeSecondsInPast);

            posix.utimes(file.getAbsolutePath(), null, null);

            fileStat = posix.stat(file.getPath());
            assertTrue("access time should be updated to current time",
                    timeWithinRange(fileStat.atime(), atimeSeconds, 10));
            assertTrue("modification time should be updated to current time",
                    timeWithinRange(fileStat.mtime(), mtimeSeconds, 10));
        } finally {
            file.delete();
        }
    }

    private boolean timeWithinRange(long actual, long expected, long precision) {
        return actual > (expected - precision) && actual < (expected + precision);
    }
}
