/*
 * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
 * (the "License"). You may not use this work except in compliance with the License, which is
 * available at www.apache.org/licenses/LICENSE-2.0
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied, as more fully set forth in the License.
 *
 * See the NOTICE file distributed with this work for information regarding copyright ownership.
 */

package alluxio.client.cli.fs;

import static org.junit.Assert.assertEquals;

import alluxio.AlluxioURI;
import alluxio.Constants;
import alluxio.cli.Command;
import alluxio.cli.fs.FileSystemShell;
import alluxio.cli.fs.FileSystemShellUtils;
import alluxio.client.file.FileSystem;
import alluxio.client.file.FileSystemContext;
import alluxio.client.file.FileSystemTestUtils;
import alluxio.conf.AlluxioConfiguration;
import alluxio.conf.Configuration;
import alluxio.exception.AlluxioException;
import alluxio.grpc.CreateDirectoryPOptions;
import alluxio.grpc.DeletePOptions;
import alluxio.grpc.WritePType;
import alluxio.master.LocalAlluxioCluster;
import alluxio.testutils.LocalAlluxioClusterResource;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.io.FileUtils;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.reflections.Reflections;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;

/**
 * Unit tests on {@link FileSystemShellUtils}.
 *
 * Note that the test case for
 * {@link FileSystemShellUtils#validatePath(String, AlluxioConfiguration)} is already covered
 * in {@link FileSystemShellUtils#getFilePath(String, AlluxioConfiguration)}.
 * Hence, only getFilePathTest is specified.
 */
public final class FileSystemShellUtilsTest {
  public static final String TEST_DIR = "/testDir";

  @Rule
  public LocalAlluxioClusterResource mLocalAlluxioClusterResource =
      new LocalAlluxioClusterResource.Builder().build();
  private FileSystem mFileSystem = null;

  @Before
  public final void before() throws Exception {
    mFileSystem = mLocalAlluxioClusterResource.get().getClient();
  }

  @Test
  public void getFilePath() throws IOException {
    String[] paths =
        new String[] {Constants.HEADER + "localhost:19998/dir", "/dir", "dir"};
    String expected = "/dir";
    for (String path : paths) {
      String result = FileSystemShellUtils.getFilePath(path, Configuration.global());
      assertEquals(expected, result);
    }
  }

  /** Type of file system. */
  public enum FsType {
    TFS, LOCAL
  }

  // TODO(binfan): rename resetFileHierarchy to resetFileSystemHierarchy
  // TODO(binfan): use option idiom for FileSystem fs, WriteType writeType
  // TODO(binfan): move those static methods to a util class
  public String resetFileHierarchy() throws IOException, AlluxioException {
    return resetFileHierarchy(mFileSystem);
  }

  /**
   * Resets the file hierarchy.
   *
   * @param fs the file system
   * @return the test directory
   */
  public static String resetFileHierarchy(FileSystem fs) throws IOException, AlluxioException {
    return resetFileHierarchy(fs, WritePType.CACHE_THROUGH);
  }

  /**
   * Resets the file hierarchy.
   *
   * @param fs the file system
   * @param writeType write types for creating a file in Alluxio
   * @return the test directory
   */
  public static String resetFileHierarchy(FileSystem fs, WritePType writeType)
      throws IOException, AlluxioException {
    /**
     * Generate such local structure TEST_DIR
     *                                ├── foo |
     *                                        ├── foobar1
     *                                        └── foobar2
     *                                ├── bar |
     *                                        └── foobar3
     *                                └── foobar4
     */
    if (fs.exists(new AlluxioURI(TEST_DIR))) {
      fs.delete(new AlluxioURI(TEST_DIR), DeletePOptions.newBuilder().setRecursive(true).build());
    }
    CreateDirectoryPOptions dirOptions = CreateDirectoryPOptions.getDefaultInstance().toBuilder()
        .setWriteType(writeType).build();
    fs.createDirectory(new AlluxioURI(TEST_DIR), dirOptions);
    fs.createDirectory(new AlluxioURI(TEST_DIR + "/foo"), dirOptions);
    fs.createDirectory(new AlluxioURI(TEST_DIR + "/bar"), dirOptions);

    FileSystemTestUtils.createByteFile(fs, TEST_DIR + "/foo/foobar1", writeType, 10);
    FileSystemTestUtils.createByteFile(fs, TEST_DIR + "/foo/foobar2", writeType, 20);
    FileSystemTestUtils.createByteFile(fs, TEST_DIR + "/bar/foobar3", writeType, 30);
    FileSystemTestUtils.createByteFile(fs, TEST_DIR + "/foobar4", writeType, 40);
    return TEST_DIR;
  }

  /**
   * Resets the local file hierarchy.
   *
   * @return the local test directory
   */
  public String resetLocalFileHierarchy() throws IOException {
    return resetLocalFileHierarchy(mLocalAlluxioClusterResource.get());
  }

  /**
   * Resets the local file hierarchy.
   *
   * @param localAlluxioCluster local Alluxio cluster for tests
   * @return the local test directory
   */
  public static String resetLocalFileHierarchy(LocalAlluxioCluster localAlluxioCluster)
      throws IOException {
    /**
     * Generate such local structure TEST_DIR
     *                                ├── foo |
     *                                        ├── foobar1
     *                                        └── foobar2
     *                                ├── bar |
     *                                        └── foobar3
     *                                └── foobar4
     */
    FileUtils.deleteDirectory(new File(localAlluxioCluster.getAlluxioHome() + TEST_DIR));
    new File(localAlluxioCluster.getAlluxioHome() + TEST_DIR).mkdir();
    new File(localAlluxioCluster.getAlluxioHome() + TEST_DIR + "/foo").mkdir();
    new File(localAlluxioCluster.getAlluxioHome() + TEST_DIR + "/bar").mkdir();

    new File(localAlluxioCluster.getAlluxioHome() + TEST_DIR + "/foo/foobar1").createNewFile();
    new File(localAlluxioCluster.getAlluxioHome() + TEST_DIR + "/foo/foobar2").createNewFile();
    new File(localAlluxioCluster.getAlluxioHome() + TEST_DIR + "/bar/foobar3").createNewFile();
    new File(localAlluxioCluster.getAlluxioHome() + TEST_DIR + "/foobar4").createNewFile();

    return localAlluxioCluster.getAlluxioHome() + TEST_DIR;
  }

  /**
   * Gets all the file paths that match the inputPath depending on fsType.
   *
   * @param path the input path
   * @param fsType the type of file system
   * @return a list of files that matches inputPath
   */
  public List<String> getPaths(String path, FsType fsType) throws IOException {
    List<String> ret = null;
    if (fsType == FsType.TFS) {
      List<AlluxioURI> tPaths =
          FileSystemShellUtils.getAlluxioURIs(mFileSystem, new AlluxioURI(path));
      ret = new ArrayList<>(tPaths.size());
      for (AlluxioURI tPath : tPaths) {
        ret.add(tPath.getPath());
      }
    } else if (fsType == FsType.LOCAL) {
      List<File> tPaths = FileSystemShellUtils.getFiles(path);
      ret = new ArrayList<>(tPaths.size());
      for (File tPath : tPaths) {
        ret.add(tPath.getPath());
      }
    }
    Collections.sort(ret);
    return ret;
  }

  /**
   * Resets the file hierarchy depending on the type of file system.
   *
   * @param fsType the type of file system
   * @return the test directory, null if the fsType is invalid
   */
  @Nullable
  public String resetFsHierarchy(FsType fsType) throws IOException, AlluxioException {
    if (fsType == FsType.TFS) {
      return resetFileHierarchy();
    } else if (fsType == FsType.LOCAL) {
      return resetLocalFileHierarchy();
    } else {
      return null;
    }
  }

  @Test
  public void getPath() throws IOException, AlluxioException {
    for (FsType fsType : FsType.values()) {
      String rootDir = resetFsHierarchy(fsType);

      List<String> tl1 = getPaths(rootDir + "/foo", fsType);
      assertEquals(tl1.size(), 1);
      assertEquals(tl1.get(0), rootDir + "/foo");

      // Trailing slash
      List<String> tl2 = getPaths(rootDir + "/foo/", fsType);
      assertEquals(tl2.size(), 1);
      assertEquals(tl2.get(0), rootDir + "/foo");

      // Wildcard
      List<String> tl3 = getPaths(rootDir + "/foo/*", fsType);
      assertEquals(tl3.size(), 2);
      assertEquals(tl3.get(0), rootDir + "/foo/foobar1");
      assertEquals(tl3.get(1), rootDir + "/foo/foobar2");

      // Trailing slash + wildcard
      List<String> tl4 = getPaths(rootDir + "/foo/*/", fsType);
      assertEquals(tl4.size(), 2);
      assertEquals(tl4.get(0), rootDir + "/foo/foobar1");
      assertEquals(tl4.get(1), rootDir + "/foo/foobar2");

      // Multiple wildcards
      List<String> tl5 = getPaths(rootDir + "/*/foo*", fsType);
      assertEquals(tl5.size(), 3);
      assertEquals(tl5.get(0), rootDir + "/bar/foobar3");
      assertEquals(tl5.get(1), rootDir + "/foo/foobar1");
      assertEquals(tl5.get(2), rootDir + "/foo/foobar2");
    }
  }

  @Test
  public void match() {
    assertEquals(FileSystemShellUtils.match("/a/b/c", "/a/*"), true);
    assertEquals(FileSystemShellUtils.match("/a/b/c", "/a/*/"), true);
    assertEquals(FileSystemShellUtils.match("/a/b/c", "/a/*/c"), true);
    assertEquals(FileSystemShellUtils.match("/a/b/c", "/a/*/*"), true);
    assertEquals(FileSystemShellUtils.match("/a/b/c", "/a/*/*/"), true);
    assertEquals(FileSystemShellUtils.match("/a/b/c/", "/a/*/*/"), true);
    assertEquals(FileSystemShellUtils.match("/a/b/c/", "/a/*/*"), true);

    assertEquals(FileSystemShellUtils.match("/foo/bar/foobar/", "/foo*/*"), true);
    assertEquals(FileSystemShellUtils.match("/foo/bar/foobar/", "/*/*/foobar"), true);

    assertEquals(FileSystemShellUtils.match("/a/b/c/", "/b/*"), false);
    assertEquals(FileSystemShellUtils.match("/", "/*/*"), false);

    assertEquals(FileSystemShellUtils.match("/a/b/c", "*"), true);
    assertEquals(FileSystemShellUtils.match("/", "/*"), true);
  }

  @Test
  public void getIntArgTest() throws Exception {
    Option opt = Option.builder("t")
        .longOpt("test")
        .numberOfArgs(1)
        .required(false)
        .build();

    CommandLine cmdLine = getCmdLine(opt, "--test", "1");
    assertEquals("Should get long form", 1, FileSystemShellUtils.getIntArg(cmdLine, opt, 0));

    cmdLine = getCmdLine(opt, "-t", "5");
    assertEquals("Should get short form", 5, FileSystemShellUtils.getIntArg(cmdLine, opt, 0));

    cmdLine = getCmdLine(opt);
    assertEquals("Should not get arg", 0, FileSystemShellUtils.getIntArg(cmdLine, opt, 0));
  }

  CommandLine getCmdLine(Option opt, String... args) throws Exception {
    CommandLineParser parser = new DefaultParser();
    return parser.parse(new Options().addOption(opt), args);
  }

  @Test
  public void loadCommands() {
    Map<String, Command> map =
        FileSystemShellUtils.loadCommands(FileSystemContext.create(Configuration.global()));

    String pkgName = Command.class.getPackage().getName();
    Reflections reflections = new Reflections(pkgName);
    Set<Class<? extends Command>> cmdSet = reflections.getSubTypesOf(Command.class);
    for (Map.Entry<String, Command> entry : map.entrySet()) {
      assertEquals(entry.getValue().getCommandName(), entry.getKey());
      assertEquals(cmdSet.contains(entry.getValue().getClass()), true);
    }

    int expectSize = 0;
    for (Class<? extends Command> cls : cmdSet) {
      if (cls.getPackage().getName()
          .equals(FileSystemShell.class.getPackage().getName() + ".command")
          && !Modifier.isAbstract(cls.getModifiers())) {
        expectSize++;
      }
    }
    assertEquals(expectSize, map.size());
  }
}

