/*
 * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

/* @test
 * @bug 8006884
 * @summary Unit test for java.nio.file.Files
 * @library ..
 * @build PassThroughFileSystem FaultyFileSystem
 * @run testng StreamTest
 */

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.Charset;
import java.nio.charset.MalformedInputException;
import java.nio.file.DirectoryIteratorException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystemLoopException;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.BiPredicate;
import java.util.stream.Stream;
import java.util.stream.Collectors;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import static org.testng.Assert.*;

@Test(groups = "unit")
public class StreamTest {
    /**
     * Default test folder
     * testFolder - empty
     *            - file
     *            - dir - d1
     *                  - f1
     *                  - lnDir2 (../dir2)
     *            - dir2
     *            - linkDir (./dir)
     *            - linkFile(./file)
     */
    static Path testFolder;
    static boolean supportsLinks;
    static Path[] level1;
    static Path[] all;
    static Path[] all_folowLinks;

    @BeforeClass
    void setupTestFolder() throws IOException {
        testFolder = TestUtil.createTemporaryDirectory();
        supportsLinks = TestUtil.supportsLinks(testFolder);
        TreeSet<Path> set = new TreeSet<>();

        // Level 1
        Path empty = testFolder.resolve("empty");
        Path file = testFolder.resolve("file");
        Path dir = testFolder.resolve("dir");
        Path dir2 = testFolder.resolve("dir2");
        Files.createDirectory(empty);
        Files.createFile(file);
        Files.createDirectory(dir);
        Files.createDirectory(dir2);
        set.add(empty);
        set.add(file);
        set.add(dir);
        set.add(dir2);
        if (supportsLinks) {
            Path tmp = testFolder.resolve("linkDir");
            Files.createSymbolicLink(tmp, dir);
            set.add(tmp);
            tmp = testFolder.resolve("linkFile");
            Files.createSymbolicLink(tmp, file);
            set.add(tmp);
        }
        level1 = set.toArray(new Path[0]);

        // Level 2
        Path tmp = dir.resolve("d1");
        Files.createDirectory(tmp);
        set.add(tmp);
        tmp = dir.resolve("f1");
        Files.createFile(tmp);
        set.add(tmp);
        if (supportsLinks) {
            tmp = dir.resolve("lnDir2");
            Files.createSymbolicLink(tmp, dir2);
            set.add(tmp);
        }
        // walk include starting folder
        set.add(testFolder);
        all = set.toArray(new Path[0]);

        // Follow links
        if (supportsLinks) {
            tmp = testFolder.resolve("linkDir");
            set.add(tmp.resolve("d1"));
            set.add(tmp.resolve("f1"));
            tmp = tmp.resolve("lnDir2");
            set.add(tmp);
        }
        all_folowLinks = set.toArray(new Path[0]);
    }

    @AfterClass
    void cleanupTestFolder() throws IOException {
        TestUtil.removeAll(testFolder);
    }

    public void testBasic() {
        try (Stream<Path> s = Files.list(testFolder)) {
            Object[] actual = s.sorted().toArray();
            assertEquals(actual, level1);
        } catch (IOException ioe) {
            fail("Unexpected IOException");
        }

        try (Stream<Path> s = Files.list(testFolder.resolve("empty"))) {
            int count = s.mapToInt(p -> 1).reduce(0, Integer::sum);
            assertEquals(count, 0, "Expect empty stream.");
        } catch (IOException ioe) {
            fail("Unexpected IOException");
        }
    }

    public void testWalk() {
        try (Stream<Path> s = Files.walk(testFolder)) {
            Object[] actual = s.sorted().toArray();
            assertEquals(actual, all);
        } catch (IOException ioe) {
            fail("Unexpected IOException");
        }
    }

    public void testWalkOneLevel() {
        try (Stream<Path> s = Files.walk(testFolder, 1)) {
            Object[] actual = s.filter(path -> ! path.equals(testFolder))
                               .sorted()
                               .toArray();
            assertEquals(actual, level1);
        } catch (IOException ioe) {
            fail("Unexpected IOException");
        }
    }

    public void testWalkFollowLink() {
        // If link is not supported, the directory structure won't have link.
        // We still want to test the behavior with FOLLOW_LINKS option.
        try (Stream<Path> s = Files.walk(testFolder, FileVisitOption.FOLLOW_LINKS)) {
            Object[] actual = s.sorted().toArray();
            assertEquals(actual, all_folowLinks);
        } catch (IOException ioe) {
            fail("Unexpected IOException");
        }
    }

    private void validateFileSystemLoopException(Path start, Path... causes) {
        try (Stream<Path> s = Files.walk(start, FileVisitOption.FOLLOW_LINKS)) {
            try {
                int count = s.mapToInt(p -> 1).reduce(0, Integer::sum);
                fail("Should got FileSystemLoopException, but got " + count + "elements.");
            } catch (UncheckedIOException uioe) {
                IOException ioe = uioe.getCause();
                if (ioe instanceof FileSystemLoopException) {
                    FileSystemLoopException fsle = (FileSystemLoopException) ioe;
                    boolean match = false;
                    for (Path cause: causes) {
                        if (fsle.getFile().equals(cause.toString())) {
                            match = true;
                            break;
                        }
                    }
                    assertTrue(match);
                } else {
                    fail("Unexpected UncheckedIOException cause " + ioe.toString());
                }
            }
        } catch(IOException ex) {
            fail("Unexpected IOException " + ex);
        }
    }

    public void testWalkFollowLinkLoop() {
        if (!supportsLinks) {
            return;
        }

        // Loops.
        try {
            Path dir = testFolder.resolve("dir");
            Path linkdir = testFolder.resolve("linkDir");
            Path d1 = dir.resolve("d1");
            Path cause = d1.resolve("lnSelf");
            Files.createSymbolicLink(cause, d1);

            // loop in descendant.
            validateFileSystemLoopException(dir, cause);
            // loop in self
            validateFileSystemLoopException(d1, cause);
            // start from other place via link
            validateFileSystemLoopException(linkdir,
                    linkdir.resolve(Paths.get("d1", "lnSelf")));
            Files.delete(cause);

            // loop to parent.
            cause = d1.resolve("lnParent");
            Files.createSymbolicLink(cause, dir);

            // loop should be detected at test/dir/d1/lnParent/d1
            validateFileSystemLoopException(d1, cause.resolve("d1"));
            // loop should be detected at link
            validateFileSystemLoopException(dir, cause);
            // loop should be detected at test/linkdir/d1/lnParent
            // which is test/dir we have visited via test/linkdir
            validateFileSystemLoopException(linkdir,
                    linkdir.resolve(Paths.get("d1", "lnParent")));
            Files.delete(cause);

            // cross loop
            Path dir2 = testFolder.resolve("dir2");
            cause = dir2.resolve("lnDir");
            Files.createSymbolicLink(cause, dir);
            validateFileSystemLoopException(dir,
                    dir.resolve(Paths.get("lnDir2", "lnDir")));
            validateFileSystemLoopException(dir2,
                    dir2.resolve(Paths.get("lnDir", "lnDir2")));
            validateFileSystemLoopException(linkdir,
                    linkdir.resolve(Paths.get("lnDir2", "lnDir")));
        } catch(IOException ioe) {
            fail("Unexpected IOException " + ioe);
        }
    }

    private static class PathBiPredicate implements BiPredicate<Path, BasicFileAttributes> {
        private final BiPredicate<Path, BasicFileAttributes> pred;
        private final Set<Path> visited = new TreeSet<Path>();

        PathBiPredicate(BiPredicate<Path, BasicFileAttributes> pred) {
            this.pred = Objects.requireNonNull(pred);
        }

        public boolean test(Path path, BasicFileAttributes attrs) {
            visited.add(path);
            return pred.test(path, attrs);
        }

        public Path[] visited() {
            return visited.toArray(new Path[0]);
        }
    }

    public void testFind() throws IOException {
        PathBiPredicate pred = new PathBiPredicate((path, attrs) -> true);

        try (Stream<Path> s = Files.find(testFolder, Integer.MAX_VALUE, pred)) {
            Set<Path> result = s.collect(Collectors.toCollection(TreeSet::new));
            assertEquals(pred.visited(), all);
            assertEquals(result.toArray(new Path[0]), pred.visited());
        }

        pred = new PathBiPredicate((path, attrs) -> attrs.isSymbolicLink());
        try (Stream<Path> s = Files.find(testFolder, Integer.MAX_VALUE, pred)) {
            s.forEach(path -> assertTrue(Files.isSymbolicLink(path)));
            assertEquals(pred.visited(), all);
        }

        pred = new PathBiPredicate((path, attrs) ->
            path.getFileName().toString().startsWith("e"));
        try (Stream<Path> s = Files.find(testFolder, Integer.MAX_VALUE, pred)) {
            s.forEach(path -> assertEquals(path.getFileName().toString(), "empty"));
            assertEquals(pred.visited(), all);
        }

        pred = new PathBiPredicate((path, attrs) ->
            path.getFileName().toString().startsWith("l") && attrs.isRegularFile());
        try (Stream<Path> s = Files.find(testFolder, Integer.MAX_VALUE, pred)) {
            s.forEach(path -> fail("Expect empty stream"));
            assertEquals(pred.visited(), all);
        }
    }

    // Test borrowed from BytesAndLines
    public void testLines() throws IOException {
        final Charset US_ASCII = Charset.forName("US-ASCII");
        Path tmpfile = Files.createTempFile("blah", "txt");

        try {
            // zero lines
            assertTrue(Files.size(tmpfile) == 0, "File should be empty");
            try (Stream<String> s = Files.lines(tmpfile, US_ASCII)) {
                assertEquals(s.mapToInt(l -> 1).reduce(0, Integer::sum), 0, "No line expected");
            }

            // one line
            byte[] hi = { (byte)'h', (byte)'i' };
            Files.write(tmpfile, hi);
            try (Stream<String> s = Files.lines(tmpfile, US_ASCII)) {
                List<String> lines = s.collect(Collectors.toList());
                assertTrue(lines.size() == 1, "One line expected");
                assertTrue(lines.get(0).equals("hi"), "'Hi' expected");
            }

            // two lines using platform's line separator
            List<String> expected = Arrays.asList("hi", "there");
            Files.write(tmpfile, expected, US_ASCII);
            assertTrue(Files.size(tmpfile) > 0, "File is empty");
            try (Stream<String> s = Files.lines(tmpfile, US_ASCII)) {
                List<String> lines = s.collect(Collectors.toList());
                assertTrue(lines.equals(expected), "Unexpected lines");
            }

            // MalformedInputException
            byte[] bad = { (byte)0xff, (byte)0xff };
            Files.write(tmpfile, bad);
            try (Stream<String> s = Files.lines(tmpfile, US_ASCII)) {
                try {
                    List<String> lines = s.collect(Collectors.toList());
                    throw new RuntimeException("UncheckedIOException expected");
                } catch (UncheckedIOException ex) {
                    assertTrue(ex.getCause() instanceof MalformedInputException,
                               "MalformedInputException expected");
                }
            }

            // NullPointerException
            try {
                Files.lines(null, US_ASCII);
                throw new RuntimeException("NullPointerException expected");
            } catch (NullPointerException ignore) { }
            try {
                Files.lines(tmpfile, null);
                throw new RuntimeException("NullPointerException expected");
            } catch (NullPointerException ignore) { }

        } finally {
            Files.delete(tmpfile);
        }
    }

    public void testDirectoryIteratorException() throws IOException {
        Path dir = testFolder.resolve("dir2");
        Path trigger = dir.resolve("DirectoryIteratorException");
        Files.createFile(trigger);
        FaultyFileSystem.FaultyFSProvider fsp = FaultyFileSystem.FaultyFSProvider.getInstance();
        FaultyFileSystem fs = (FaultyFileSystem) fsp.newFileSystem(dir, null);

        try {
            fsp.setFaultyMode(false);
            Path fakeRoot = fs.getRoot();
            try {
                try (Stream<Path> s = Files.list(fakeRoot)) {
                    s.forEach(path -> assertEquals(path.getFileName().toString(), "DirectoryIteratorException"));
                }
            } catch (UncheckedIOException uioe) {
                fail("Unexpected exception.");
            }

            fsp.setFaultyMode(true);
            try {
                try (DirectoryStream<Path> ds = Files.newDirectoryStream(fakeRoot)) {
                    Iterator<Path> itor = ds.iterator();
                    while (itor.hasNext()) {
                        itor.next();
                    }
                }
                fail("Shoule throw DirectoryIteratorException");
            } catch (DirectoryIteratorException die) {
            }

            try {
                try (Stream<Path> s = Files.list(fakeRoot)) {
                    s.forEach(path -> fail("should not get here"));
                }
            } catch (UncheckedIOException uioe) {
                assertTrue(uioe.getCause() instanceof FaultyFileSystem.FaultyException);
            } catch (DirectoryIteratorException die) {
                fail("Should have been converted into UncheckedIOException.");
            }
        } finally {
            // Cleanup
            if (fs != null) {
                fs.close();
            }
            Files.delete(trigger);
        }
    }

    public void testUncheckedIOException() throws IOException {
        Path triggerFile = testFolder.resolve(Paths.get("dir2", "IOException"));
        Files.createFile(triggerFile);
        Path triggerDir = testFolder.resolve(Paths.get("empty", "IOException"));
        Files.createDirectories(triggerDir);
        Files.createFile(triggerDir.resolve("file"));
        FaultyFileSystem.FaultyFSProvider fsp = FaultyFileSystem.FaultyFSProvider.getInstance();
        FaultyFileSystem fs = (FaultyFileSystem) fsp.newFileSystem(testFolder, null);

        try {
            fsp.setFaultyMode(false);
            Path fakeRoot = fs.getRoot();
            try (Stream<Path> s = Files.list(fakeRoot.resolve("dir2"))) {
                // only one file
                s.forEach(path -> assertEquals(path.getFileName().toString(), "IOException"));
            }

            try (Stream<Path> s = Files.walk(fakeRoot.resolve("empty"))) {
                String[] result = s.map(path -> path.getFileName().toString())
                                   .toArray(String[]::new);
                // ordered as depth-first
                assertEquals(result, new String[] { "empty", "IOException", "file"});
            }

            fsp.setFaultyMode(true);
            try (Stream<Path> s = Files.list(fakeRoot.resolve("dir2"))) {
                s.forEach(path -> fail("should have caused exception"));
            } catch (UncheckedIOException uioe) {
                assertTrue(uioe.getCause() instanceof FaultyFileSystem.FaultyException);
            }

            try (Stream<Path> s = Files.walk(fakeRoot.resolve("empty"))) {
                String[] result = s.map(path -> path.getFileName().toString())
                                   .toArray(String[]::new);
                fail("should not reach here due to IOException");
            } catch (UncheckedIOException uioe) {
                assertTrue(uioe.getCause() instanceof FaultyFileSystem.FaultyException);
            }

            try (Stream<Path> s = Files.walk(
                fakeRoot.resolve("empty").resolve("IOException")))
            {
                String[] result = s.map(path -> path.getFileName().toString())
                                   .toArray(String[]::new);
                fail("should not reach here due to IOException");
            } catch (IOException ioe) {
                assertTrue(ioe instanceof FaultyFileSystem.FaultyException);
            } catch (UncheckedIOException ex) {
                fail("Top level should be repored as is");
            }
         } finally {
            // Cleanup
            if (fs != null) {
                fs.close();
            }
            Files.delete(triggerFile);
            TestUtil.removeAll(triggerDir);
        }
    }

    public void testSecurityException() throws IOException {
        Path empty = testFolder.resolve("empty");
        Path triggerFile = Files.createFile(empty.resolve("SecurityException"));
        Path sampleFile = Files.createDirectories(empty.resolve("sample"));

        Path dir2 = testFolder.resolve("dir2");
        Path triggerDir = Files.createDirectories(dir2.resolve("SecurityException"));
        Files.createFile(triggerDir.resolve("fileInSE"));
        Path sample = Files.createFile(dir2.resolve("file"));

        Path triggerLink = null;
        Path linkTriggerDir = null;
        Path linkTriggerFile = null;
        if (supportsLinks) {
            Path dir = testFolder.resolve("dir");
            triggerLink = Files.createSymbolicLink(dir.resolve("SecurityException"), empty);
            linkTriggerDir = Files.createSymbolicLink(dir.resolve("lnDirSE"), triggerDir);
            linkTriggerFile = Files.createSymbolicLink(dir.resolve("lnFileSE"), triggerFile);
        }

        FaultyFileSystem.FaultyFSProvider fsp = FaultyFileSystem.FaultyFSProvider.getInstance();
        FaultyFileSystem fs = (FaultyFileSystem) fsp.newFileSystem(testFolder, null);

        try {
            fsp.setFaultyMode(false);
            Path fakeRoot = fs.getRoot();
            // validate setting
            try (Stream<Path> s = Files.list(fakeRoot.resolve("empty"))) {
                String[] result = s.map(path -> path.getFileName().toString())
                                   .toArray(String[]::new);
                assertEqualsNoOrder(result, new String[] { "SecurityException", "sample" });
            }

            try (Stream<Path> s = Files.walk(fakeRoot.resolve("dir2"))) {
                String[] result = s.map(path -> path.getFileName().toString())
                                   .toArray(String[]::new);
                assertEqualsNoOrder(result, new String[] { "dir2", "SecurityException", "fileInSE", "file" });
            }

            if (supportsLinks) {
                try (Stream<Path> s = Files.list(fakeRoot.resolve("dir"))) {
                    String[] result = s.map(path -> path.getFileName().toString())
                                       .toArray(String[]::new);
                    assertEqualsNoOrder(result, new String[] { "d1", "f1", "lnDir2", "SecurityException", "lnDirSE", "lnFileSE" });
                }
            }

            // execute test
            fsp.setFaultyMode(true);
            // ignore file cause SecurityException
            try (Stream<Path> s = Files.walk(fakeRoot.resolve("empty"))) {
                String[] result = s.map(path -> path.getFileName().toString())
                                   .toArray(String[]::new);
                assertEqualsNoOrder(result, new String[] { "empty", "sample" });
            }
            // skip folder cause SecurityException
            try (Stream<Path> s = Files.walk(fakeRoot.resolve("dir2"))) {
                String[] result = s.map(path -> path.getFileName().toString())
                                   .toArray(String[]::new);
                assertEqualsNoOrder(result, new String[] { "dir2", "file" });
            }

            if (supportsLinks) {
                // not following links
                try (Stream<Path> s = Files.walk(fakeRoot.resolve("dir"))) {
                    String[] result = s.map(path -> path.getFileName().toString())
                                       .toArray(String[]::new);
                    assertEqualsNoOrder(result, new String[] { "dir", "d1", "f1", "lnDir2", "lnDirSE", "lnFileSE" });
                }

                // following links
                try (Stream<Path> s = Files.walk(fakeRoot.resolve("dir"), FileVisitOption.FOLLOW_LINKS)) {
                    String[] result = s.map(path -> path.getFileName().toString())
                                       .toArray(String[]::new);
                    // ?? Should fileInSE show up?
                    // With FaultyFS, it does as no exception thrown for link to "SecurityException" with read on "lnXxxSE"
                    assertEqualsNoOrder(result, new String[] { "dir", "d1", "f1", "lnDir2", "file", "lnDirSE", "lnFileSE", "fileInSE" });
                }
            }

            // list instead of walk
            try (Stream<Path> s = Files.list(fakeRoot.resolve("empty"))) {
                String[] result = s.map(path -> path.getFileName().toString())
                                   .toArray(String[]::new);
                assertEqualsNoOrder(result, new String[] { "sample" });
            }
            try (Stream<Path> s = Files.list(fakeRoot.resolve("dir2"))) {
                String[] result = s.map(path -> path.getFileName().toString())
                                   .toArray(String[]::new);
                assertEqualsNoOrder(result, new String[] { "file" });
            }

            // root cause SecurityException should be reported
            try (Stream<Path> s = Files.walk(
                fakeRoot.resolve("dir2").resolve("SecurityException")))
            {
                String[] result = s.map(path -> path.getFileName().toString())
                                   .toArray(String[]::new);
                fail("should not reach here due to SecurityException");
            } catch (SecurityException se) {
                assertTrue(se.getCause() instanceof FaultyFileSystem.FaultyException);
            }

            // Walk a file cause SecurityException, we should get SE
            try (Stream<Path> s = Files.walk(
                fakeRoot.resolve("dir").resolve("SecurityException")))
            {
                String[] result = s.map(path -> path.getFileName().toString())
                                   .toArray(String[]::new);
                fail("should not reach here due to SecurityException");
            } catch (SecurityException se) {
                assertTrue(se.getCause() instanceof FaultyFileSystem.FaultyException);
            }

            // List a file cause SecurityException, we should get SE as cannot read attribute
            try (Stream<Path> s = Files.list(
                fakeRoot.resolve("dir2").resolve("SecurityException")))
            {
                String[] result = s.map(path -> path.getFileName().toString())
                                   .toArray(String[]::new);
                fail("should not reach here due to SecurityException");
            } catch (SecurityException se) {
                assertTrue(se.getCause() instanceof FaultyFileSystem.FaultyException);
            }

            try (Stream<Path> s = Files.list(
                fakeRoot.resolve("dir").resolve("SecurityException")))
            {
                String[] result = s.map(path -> path.getFileName().toString())
                                   .toArray(String[]::new);
                fail("should not reach here due to SecurityException");
            } catch (SecurityException se) {
                assertTrue(se.getCause() instanceof FaultyFileSystem.FaultyException);
            }
         } finally {
            // Cleanup
            if (fs != null) {
                fs.close();
            }
            if (supportsLinks) {
                Files.delete(triggerLink);
                Files.delete(linkTriggerDir);
                Files.delete(linkTriggerFile);
            }
            Files.delete(triggerFile);
            Files.delete(sampleFile);
            Files.delete(sample);
            TestUtil.removeAll(triggerDir);
        }
    }

    public void testConstructException() {
        try (Stream<String> s = Files.lines(testFolder.resolve("notExist"), Charset.forName("UTF-8"))) {
            s.forEach(l -> fail("File is not even exist!"));
        } catch (IOException ioe) {
            assertTrue(ioe instanceof NoSuchFileException);
        }
    }

    public void testClosedStream() throws IOException {
        try (Stream<Path> s = Files.list(testFolder)) {
            s.close();
            Object[] actual = s.sorted().toArray();
            fail("Operate on closed stream should throw IllegalStateException");
        } catch (IllegalStateException ex) {
            // expected
        }

        try (Stream<Path> s = Files.walk(testFolder)) {
            s.close();
            Object[] actual = s.sorted().toArray();
            fail("Operate on closed stream should throw IllegalStateException");
        } catch (IllegalStateException ex) {
            // expected
        }

        try (Stream<Path> s = Files.find(testFolder, Integer.MAX_VALUE,
                    (p, attr) -> true)) {
            s.close();
            Object[] actual = s.sorted().toArray();
            fail("Operate on closed stream should throw IllegalStateException");
        } catch (IllegalStateException ex) {
            // expected
        }
    }
}
