/*
 * 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.lucene.tests.mockfile;

import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;

/** Basic tests for ShuffleFS */
public class TestShuffleFS extends MockFileSystemTestCase {

  @Override
  protected Path wrap(Path path) {
    return wrap(path, random().nextLong());
  }

  Path wrap(Path path, long seed) {
    ShuffleFS provider = new ShuffleFS(path.getFileSystem(), seed);
    return provider.wrapPath(path);
  }

  /** test that we return directory listings correctly */
  public void testShuffleWorks() throws IOException {
    Path dir = wrap(createTempDir());

    Files.createFile(dir.resolve("file1"));
    Files.createFile(dir.resolve("file2"));
    Files.createFile(dir.resolve("file3"));

    List<Path> seen = new ArrayList<>();
    try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
      for (Path path : stream) {
        seen.add(path);
      }
    }

    assertEquals(3, seen.size());
    assertTrue(seen.contains(dir.resolve("file1")));
    assertTrue(seen.contains(dir.resolve("file2")));
    assertTrue(seen.contains(dir.resolve("file3")));
  }

  /** test that we change order of directory listings */
  public void testActuallyShuffles() throws IOException {
    Path dir = createTempDir();
    for (int i = 0; i < 100; i++) {
      Files.createFile(dir.resolve("file" + i));
    }
    List<String> expected = new ArrayList<>();

    // get the raw listing from the actual filesystem
    try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
      for (Path path : stream) {
        expected.add(path.getFileName().toString());
      }
    }

    // shuffle until the order changes.
    for (int i = 0; i < 10000; i++) {
      Path wrapped = wrap(dir, random().nextLong());

      List<String> seen = new ArrayList<>();
      try (DirectoryStream<Path> stream = Files.newDirectoryStream(wrapped)) {
        for (Path path : stream) {
          seen.add(path.getFileName().toString());
        }
      }

      // we should always see the same files.
      assertEquals(new HashSet<>(expected), new HashSet<>(seen));
      if (!expected.equals(seen)) {
        return;
      }
    }
    fail("ordering never changed");
  }

  /**
   * shuffle underlying contents randomly with different seeds, and ensure shuffling that again with
   * the same seed is consistent.
   */
  public void testConsistentOrder() throws IOException {
    Path raw = createTempDir();
    for (int i = 0; i < 100; i++) {
      Files.createFile(raw.resolve("file" + i));
    }

    long seed = random().nextLong();
    Path dirExpected = wrap(raw, seed);

    // get the shuffled listing for the seed.
    List<String> expected = new ArrayList<>();
    try (DirectoryStream<Path> stream = Files.newDirectoryStream(dirExpected)) {
      for (Path path : stream) {
        expected.add(path.getFileName().toString());
      }
    }

    // shuffle wrapping a different scrambled ordering each time, it should always be the same.
    for (int i = 0; i < 100; i++) {
      Path scrambled = wrap(raw, random().nextLong());
      Path ordered = wrap(scrambled, seed);

      List<String> seen = new ArrayList<>();
      try (DirectoryStream<Path> stream = Files.newDirectoryStream(ordered)) {
        for (Path path : stream) {
          seen.add(path.getFileName().toString());
        }
      }

      // we should always see the same files in the same order
      assertEquals(expected, seen);
    }
  }

  /** test that we give a consistent order for the same file names within different directories */
  public void testFileNameOnly() throws IOException {
    Path dir = wrap(createTempDir());

    Files.createFile(dir.resolve("file1"));
    Files.createFile(dir.resolve("file2"));
    Files.createFile(dir.resolve("file3"));

    List<String> expected = new ArrayList<>();
    try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
      for (Path path : stream) {
        expected.add(path.getFileName().toString());
      }
    }

    Path subdir = dir.resolve("subdir");
    Files.createDirectory(subdir);
    Files.createFile(subdir.resolve("file3"));
    Files.createFile(subdir.resolve("file2"));
    Files.createFile(subdir.resolve("file1"));

    List<String> actual = new ArrayList<>();
    try (DirectoryStream<Path> stream = Files.newDirectoryStream(subdir)) {
      for (Path path : stream) {
        actual.add(path.getFileName().toString());
      }
    }

    assertEquals(expected, actual);
  }
}
