/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.commons.io;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.time.Duration;
import java.util.Locale;

import org.junit.jupiter.api.Test;

/**
 * Tests {@link FileSystemUtils}.
 */
@SuppressWarnings("deprecation") // testing deprecated class
public class FileSystemUtilsTest {

    static class MockFileSystemUtils extends FileSystemUtils {
        private final int exitCode;
        private final byte[] bytes;
        private final String cmd;

        public MockFileSystemUtils(final int exitCode, final String lines) {
            this(exitCode, lines, null);
        }

        public MockFileSystemUtils(final int exitCode, final String lines, final String cmd) {
            this.exitCode = exitCode;
            this.bytes = lines.getBytes();
            this.cmd = cmd;
        }

        @Override
        Process openProcess(final String[] params) {
            if (cmd != null) {
                assertEquals(cmd, params[params.length - 1]);
            }
            return new Process() {
                @Override
                public void destroy() {
                }

                @Override
                public int exitValue() {
                    return exitCode;
                }

                @Override
                public InputStream getErrorStream() {
                    return null;
                }

                @Override
                public InputStream getInputStream() {
                    return new ByteArrayInputStream(bytes);
                }

                @Override
                public OutputStream getOutputStream() {
                    return null;
                }

                @Override
                public int waitFor() throws InterruptedException {
                    return exitCode;
                }
            };
        }
    }

    private static final Duration NEG_1_TIMEOUT = Duration.ofMillis(-1);

    @Test
    public void testGetFreeSpace_String() throws Exception {
        // test coverage, as we can't check value
        if (File.separatorChar == '/') {
            // have to figure out Unix block size
            final String[] cmd;
            String osName = System.getProperty("os.name");
            osName = osName.toLowerCase(Locale.ENGLISH);

            if (osName.contains("hp-ux") || osName.contains("aix")) {
                cmd = new String[]{"df", "-P", "/"};
            } else if (osName.contains("sunos") || osName.contains("sun os")
                    || osName.contains("solaris")) {
                cmd = new String[]{"/usr/xpg4/bin/df", "-P", "/"};
            } else {
                cmd = new String[]{"df", "/"};
            }
            final Process proc = Runtime.getRuntime().exec(cmd);
            boolean kilobyteBlock = true;
            try (BufferedReader r = new BufferedReader(new InputStreamReader(proc.getInputStream()))){
                final String line = r.readLine();
                assertNotNull(line, "Unexpected null line");
                if (line.contains("512")) {
                    kilobyteBlock = false;
                }
            }

            // now perform the test
            final long free = FileSystemUtils.freeSpace("/");
            final long kb = FileSystemUtils.freeSpaceKb("/");
            // Assume disk space does not fluctuate
            // more than 1% between the above two calls;
            // this is also small enough to verify freeSpaceKb uses
            // kibibytes (1024) instead of SI kilobytes (1000)
            final double acceptableDelta = kb * 0.01d;
            if (kilobyteBlock) {
                assertEquals(free, kb, acceptableDelta);
            } else {
                assertEquals(free / 2d, kb, acceptableDelta);
            }
        } else {
            final long bytes = FileSystemUtils.freeSpace("");
            final long kb = FileSystemUtils.freeSpaceKb("");
            // Assume disk space does not fluctuate more than 1%
            final double acceptableDelta = kb * 0.01d;
            assertEquals((double) bytes / 1024, kb, acceptableDelta);
        }
    }

    @Test
    public void testGetFreeSpaceOS_String_InitError() throws Exception {
        final FileSystemUtils fsu = new FileSystemUtils();
        assertThrows(IllegalStateException.class, () -> fsu.freeSpaceOS("", -1, false, NEG_1_TIMEOUT));
        assertThrows(IllegalStateException.class, () -> fsu.freeSpaceOS("", -1, true, NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceOS_String_NullPath() throws Exception {
        final FileSystemUtils fsu = new FileSystemUtils();
        assertThrows(NullPointerException.class, () -> fsu.freeSpaceOS(null, 1, false, NEG_1_TIMEOUT));
        assertThrows(NullPointerException.class, () -> fsu.freeSpaceOS(null, 1, true, NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceOS_String_Other() throws Exception {
        final FileSystemUtils fsu = new FileSystemUtils();
        assertThrows(IllegalStateException.class, () -> fsu.freeSpaceOS("", 0, false, NEG_1_TIMEOUT));
        assertThrows(NullPointerException.class, () -> fsu.freeSpaceOS(null, 1, true, NEG_1_TIMEOUT));
        assertThrows(IllegalStateException.class, () -> fsu.freeSpaceOS("", 0, true, NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceOS_String_Unix() throws Exception {
        final FileSystemUtils fsu = new FileSystemUtils() {
            @Override
            protected long freeSpaceUnix(final String path, final boolean kb, final boolean posix, final Duration timeout) throws IOException {
                return kb ? 12345L : 54321;
            }
        };
        assertEquals(54321L, fsu.freeSpaceOS("", 2, false, NEG_1_TIMEOUT));
        assertEquals(12345L, fsu.freeSpaceOS("", 2, true, NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceOS_String_Windows() throws Exception {
        final FileSystemUtils fsu = new FileSystemUtils() {
            @Override
            protected long freeSpaceWindows(final String path, final Duration timeout) throws IOException {
                return 12345L;
            }
        };
        assertEquals(12345L, fsu.freeSpaceOS("", 1, false, NEG_1_TIMEOUT));
        assertEquals(12345L / 1024, fsu.freeSpaceOS("", 1, true, NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceUnix_String_EmptyPath() throws Exception {
        final String lines =
                "Filesystem           1K-blocks      Used Available Use% Mounted on\n" +
                        "xxx:/home/users/s     14428928  12956424   1472504  90% /home/users/s";
        final FileSystemUtils fsu = new MockFileSystemUtils(0, lines);
        assertThrows(IllegalArgumentException.class, () -> fsu.freeSpaceUnix("", false, false, NEG_1_TIMEOUT));
        assertThrows(IllegalArgumentException.class, () -> fsu.freeSpaceUnix("", true, false, NEG_1_TIMEOUT));
        assertThrows(IllegalArgumentException.class, () -> fsu.freeSpaceUnix("", true, true, NEG_1_TIMEOUT));
        assertThrows(IllegalArgumentException.class, () -> fsu.freeSpaceUnix("", false, true, NEG_1_TIMEOUT));
    }

    @Test

    public void testGetFreeSpaceUnix_String_EmptyResponse() {
        final String lines = "";
        final FileSystemUtils fsu = new MockFileSystemUtils(0, lines);
        assertThrows(IOException.class, () -> fsu.freeSpaceUnix("/home/users/s", false, false, NEG_1_TIMEOUT));
        assertThrows(IOException.class, () -> fsu.freeSpaceUnix("/home/users/s", true, false, NEG_1_TIMEOUT));
        assertThrows(IOException.class, () -> fsu.freeSpaceUnix("/home/users/s", false, true, NEG_1_TIMEOUT));
        assertThrows(IOException.class, () -> fsu.freeSpaceUnix("/home/users/s", true, true, NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceUnix_String_InvalidResponse1() {
        final String lines =
                "Filesystem           1K-blocks      Used Available Use% Mounted on\n" +
                        "                      14428928  12956424       100";
        final FileSystemUtils fsu = new MockFileSystemUtils(0, lines);
        assertThrows(IOException.class, () -> fsu.freeSpaceUnix("/home/users/s", false, false, NEG_1_TIMEOUT));
        assertThrows(IOException.class, () -> fsu.freeSpaceUnix("/home/users/s", true, false, NEG_1_TIMEOUT));
        assertThrows(IOException.class, () -> fsu.freeSpaceUnix("/home/users/s", false, true, NEG_1_TIMEOUT));
        assertThrows(IOException.class, () -> fsu.freeSpaceUnix("/home/users/s", true, true, NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceUnix_String_InvalidResponse2() {
        final String lines =
                "Filesystem           1K-blocks      Used Available Use% Mounted on\n" +
                        "xxx:/home/users/s     14428928  12956424   nnnnnnn  90% /home/users/s";
        final FileSystemUtils fsu = new MockFileSystemUtils(0, lines);
        assertThrows(IOException.class, () -> fsu.freeSpaceUnix("/home/users/s", false, false, NEG_1_TIMEOUT));
        assertThrows(IOException.class, () -> fsu.freeSpaceUnix("/home/users/s", true, false, NEG_1_TIMEOUT));
        assertThrows(IOException.class, () -> fsu.freeSpaceUnix("/home/users/s", false, true, NEG_1_TIMEOUT));
        assertThrows(IOException.class, () -> fsu.freeSpaceUnix("/home/users/s", true, true, NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceUnix_String_InvalidResponse3() {
        final String lines =
                "Filesystem           1K-blocks      Used Available Use% Mounted on\n" +
                        "xxx:/home/users/s     14428928  12956424        -1  90% /home/users/s";
        final FileSystemUtils fsu = new MockFileSystemUtils(0, lines);
        assertThrows(IOException.class, () -> fsu.freeSpaceUnix("/home/users/s", false, false, NEG_1_TIMEOUT));
        assertThrows(IOException.class, () -> fsu.freeSpaceUnix("/home/users/s", true, false, NEG_1_TIMEOUT));
        assertThrows(IOException.class, () -> fsu.freeSpaceUnix("/home/users/s", false, true, NEG_1_TIMEOUT));
        assertThrows(IOException.class, () -> fsu.freeSpaceUnix("/home/users/s", true, true, NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceUnix_String_InvalidResponse4() {
        final String lines =
                "Filesystem           1K-blocks      Used Available Use% Mounted on\n" +
                        "xxx-yyyyyyy-zzz:/home/users/s";
        final FileSystemUtils fsu = new MockFileSystemUtils(0, lines);
        assertThrows(IOException.class, () -> fsu.freeSpaceUnix("/home/users/s", false, false, NEG_1_TIMEOUT));
        assertThrows(IOException.class, () -> fsu.freeSpaceUnix("/home/users/s", true, false, NEG_1_TIMEOUT));
        assertThrows(IOException.class, () -> fsu.freeSpaceUnix("/home/users/s", false, true, NEG_1_TIMEOUT));
        assertThrows(IOException.class, () -> fsu.freeSpaceUnix("/home/users/s", true, true, NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceUnix_String_LongResponse() throws Exception {
        final String lines =
                "Filesystem           1K-blocks      Used Available Use% Mounted on\n" +
                        "xxx-yyyyyyy-zzz:/home/users/s\n" +
                        "                      14428928  12956424   1472504  90% /home/users/s";
        final FileSystemUtils fsu = new MockFileSystemUtils(0, lines);
        assertEquals(1472504L, fsu.freeSpaceUnix("/home/users/s", false, false, NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceUnix_String_LongResponseKb() throws Exception {
        final String lines =
                "Filesystem           1K-blocks      Used Available Use% Mounted on\n" +
                        "xxx-yyyyyyy-zzz:/home/users/s\n" +
                        "                      14428928  12956424   1472504  90% /home/users/s";
        final FileSystemUtils fsu = new MockFileSystemUtils(0, lines);
        assertEquals(1472504L, fsu.freeSpaceUnix("/home/users/s", true, false, NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceUnix_String_NormalResponseFreeBSD() throws Exception {
        // from Apache 'FreeBSD 6.1-RELEASE (SMP-turbo)'
        final String lines =
                "Filesystem  1K-blocks      Used    Avail Capacity  Mounted on\n" +
                        "/dev/xxxxxx    128990    102902    15770    87%    /";
        final FileSystemUtils fsu = new MockFileSystemUtils(0, lines);
        assertEquals(15770L, fsu.freeSpaceUnix("/", false, false, NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceUnix_String_NormalResponseKbFreeBSD() throws Exception {
        // from Apache 'FreeBSD 6.1-RELEASE (SMP-turbo)'
        // df and df -k are identical, but df -kP uses 512 blocks (not relevant as not used)
        final String lines =
                "Filesystem  1K-blocks      Used    Avail Capacity  Mounted on\n" +
                        "/dev/xxxxxx    128990    102902    15770    87%    /";
        final FileSystemUtils fsu = new MockFileSystemUtils(0, lines);
        assertEquals(15770L, fsu.freeSpaceUnix("/", true, false, NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceUnix_String_NormalResponseKbLinux() throws Exception {
        // from Sourceforge 'GNU bash, version 2.05b.0(1)-release (i386-redhat-linux-gnu)'
        // df, df -k and df -kP are all identical
        final String lines =
                "Filesystem           1K-blocks      Used Available Use% Mounted on\n" +
                        "/dev/xxx                497944    308528    189416  62% /";
        final FileSystemUtils fsu = new MockFileSystemUtils(0, lines);
        assertEquals(189416L, fsu.freeSpaceUnix("/", true, false, NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceUnix_String_NormalResponseKbSolaris() throws Exception {
        // from IO-91 - ' SunOS et 5.10 Generic_118822-25 sun4u sparc SUNW,Ultra-4'
        // non-kb response does not contain free space - see IO-91
        final String lines =
                "Filesystem            kbytes    used   avail capacity  Mounted on\n" +
                        "/dev/dsk/x0x0x0x0    1350955  815754  481163    63%";
        final FileSystemUtils fsu = new MockFileSystemUtils(0, lines);
        assertEquals(481163L, fsu.freeSpaceUnix("/dev/dsk/x0x0x0x0", true, false, NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceUnix_String_NormalResponseLinux() throws Exception {
        // from Sourceforge 'GNU bash, version 2.05b.0(1)-release (i386-redhat-linux-gnu)'
        final String lines =
                "Filesystem           1K-blocks      Used Available Use% Mounted on\n" +
                        "/dev/xxx                497944    308528    189416  62% /";
        final FileSystemUtils fsu = new MockFileSystemUtils(0, lines);
        assertEquals(189416L, fsu.freeSpaceUnix("/", false, false, NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceWindows_String_EmptyMultiLineResponse() {
        final String lines = "\n\n";
        final FileSystemUtils fsu = new MockFileSystemUtils(0, lines);
        assertThrows(IOException.class, () -> fsu.freeSpaceWindows("C:", NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceWindows_String_EmptyPath() throws Exception {
        final String lines =
                " Volume in drive C is HDD\n" +
                        " Volume Serial Number is XXXX-YYYY\n" +
                        "\n" +
                        " Directory of C:\\Documents and Settings\\Xxxx\n" +
                        "\n" +
                        "19/08/2005  22:43    <DIR>          .\n" +
                        "19/08/2005  22:43    <DIR>          ..\n" +
                        "11/08/2005  01:07                81 build.properties\n" +
                        "17/08/2005  21:44    <DIR>          Desktop\n" +
                        "               7 File(s)         180260 bytes\n" +
                        "              10 Dir(s)     41411551232 bytes free";
        final FileSystemUtils fsu = new MockFileSystemUtils(0, lines, "dir /a /-c ");
        assertEquals(41411551232L, fsu.freeSpaceWindows("", NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceWindows_String_EmptyResponse() {
        final String lines = "";
        final FileSystemUtils fsu = new MockFileSystemUtils(0, lines);
        assertThrows(IOException.class, () -> fsu.freeSpaceWindows("C:", NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceWindows_String_InvalidTextResponse() {
        final String lines = "BlueScreenOfDeath";
        final FileSystemUtils fsu = new MockFileSystemUtils(0, lines);
        assertThrows(IOException.class, () -> fsu.freeSpaceWindows("C:", NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceWindows_String_NormalResponse() throws Exception {
        final String lines =
                " Volume in drive C is HDD\n" +
                        " Volume Serial Number is XXXX-YYYY\n" +
                        "\n" +
                        " Directory of C:\\Documents and Settings\\Xxxx\n" +
                        "\n" +
                        "19/08/2005  22:43    <DIR>          .\n" +
                        "19/08/2005  22:43    <DIR>          ..\n" +
                        "11/08/2005  01:07                81 build.properties\n" +
                        "17/08/2005  21:44    <DIR>          Desktop\n" +
                        "               7 File(s)         180260 bytes\n" +
                        "              10 Dir(s)     41411551232 bytes free";
        final FileSystemUtils fsu = new MockFileSystemUtils(0, lines, "dir /a /-c \"C:\"");
        assertEquals(41411551232L, fsu.freeSpaceWindows("C:", NEG_1_TIMEOUT));
    }
    @Test
    public void testGetFreeSpaceWindows_String_NoSuchDirectoryResponse() {
        final String lines =
                " Volume in drive C is HDD\n" +
                        " Volume Serial Number is XXXX-YYYY\n" +
                        "\n" +
                        " Directory of C:\\Documents and Settings\\empty" +
                        "\n";
        final FileSystemUtils fsu = new MockFileSystemUtils(1, lines);
        assertThrows(IOException.class, () -> fsu.freeSpaceWindows("C:", NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceWindows_String_ParseCommaFormatBytes() throws Exception {
        // this is the format of response when calling dir /c
        // we have now switched to dir /-c, so we should never get this
        final String lines =
                " Volume in drive C is HDD\n" +
                        " Volume Serial Number is XXXX-YYYY\n" +
                        "\n" +
                        " Directory of C:\\Documents and Settings\\Xxxx\n" +
                        "\n" +
                        "19/08/2005  22:43    <DIR>          .\n" +
                        "19/08/2005  22:43    <DIR>          ..\n" +
                        "11/08/2005  01:07                81 build.properties\n" +
                        "17/08/2005  21:44    <DIR>          Desktop\n" +
                        "               7 File(s)        180,260 bytes\n" +
                        "              10 Dir(s)  41,411,551,232 bytes free";
        final FileSystemUtils fsu = new MockFileSystemUtils(0, lines);
        assertEquals(41411551232L, fsu.freeSpaceWindows("", NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceWindows_String_ParseCommaFormatBytes_Big() throws Exception {
        // test with very large free space
        final String lines =
                " Volume in drive C is HDD\n" +
                        " Volume Serial Number is XXXX-YYYY\n" +
                        "\n" +
                        " Directory of C:\\Documents and Settings\\Xxxx\n" +
                        "\n" +
                        "19/08/2005  22:43    <DIR>          .\n" +
                        "19/08/2005  22:43    <DIR>          ..\n" +
                        "11/08/2005  01:07                81 build.properties\n" +
                        "17/08/2005  21:44    <DIR>          Desktop\n" +
                        "               7 File(s)        180,260 bytes\n" +
                        "              10 Dir(s)  141,411,551,232 bytes free";
        final FileSystemUtils fsu = new MockFileSystemUtils(0, lines);
        assertEquals(141411551232L, fsu.freeSpaceWindows("", NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceWindows_String_ParseCommaFormatBytes_Small() throws Exception {
        // test with very large free space
        final String lines =
                " Volume in drive C is HDD\n" +
                        " Volume Serial Number is XXXX-YYYY\n" +
                        "\n" +
                        " Directory of C:\\Documents and Settings\\Xxxx\n" +
                        "\n" +
                        "19/08/2005  22:43    <DIR>          .\n" +
                        "19/08/2005  22:43    <DIR>          ..\n" +
                        "11/08/2005  01:07                81 build.properties\n" +
                        "17/08/2005  21:44    <DIR>          Desktop\n" +
                        "               7 File(s)        180,260 bytes\n" +
                        "              10 Dir(s)  1,232 bytes free";
        final FileSystemUtils fsu = new MockFileSystemUtils(0, lines);
        assertEquals(1232L, fsu.freeSpaceWindows("", NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceWindows_String_quoted() throws Exception {
        final String lines =
                " Volume in drive C is HDD\n" +
                        " Volume Serial Number is XXXX-YYYY\n" +
                        "\n" +
                        " Directory of C:\\Documents and Settings\\Xxxx\n" +
                        "\n" +
                        "19/08/2005  22:43    <DIR>          .\n" +
                        "19/08/2005  22:43    <DIR>          ..\n" +
                        "11/08/2005  01:07                81 build.properties\n" +
                        "17/08/2005  21:44    <DIR>          Desktop\n" +
                        "               7 File(s)         180260 bytes\n" +
                        "              10 Dir(s)     41411551232 bytes free";
        final FileSystemUtils fsu = new MockFileSystemUtils(0, lines, "dir /a /-c \"C:\\somedir\"");
        assertEquals(41411551232L, fsu.freeSpaceWindows("\"C:\\somedir\"", NEG_1_TIMEOUT));
    }

    @Test
    public void testGetFreeSpaceWindows_String_StripDrive() throws Exception {
        final String lines =
                " Volume in drive C is HDD\n" +
                        " Volume Serial Number is XXXX-YYYY\n" +
                        "\n" +
                        " Directory of C:\\Documents and Settings\\Xxxx\n" +
                        "\n" +
                        "19/08/2005  22:43    <DIR>          .\n" +
                        "19/08/2005  22:43    <DIR>          ..\n" +
                        "11/08/2005  01:07                81 build.properties\n" +
                        "17/08/2005  21:44    <DIR>          Desktop\n" +
                        "               7 File(s)         180260 bytes\n" +
                        "              10 Dir(s)     41411551232 bytes free";
        final FileSystemUtils fsu = new MockFileSystemUtils(0, lines, "dir /a /-c \"C:\\somedir\"");
        assertEquals(41411551232L, fsu.freeSpaceWindows("C:\\somedir", NEG_1_TIMEOUT));
    }

}
