/*
 * 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.fuse;

import alluxio.client.file.FileSystem;
import alluxio.client.file.FileSystemContext;
import alluxio.conf.AlluxioConfiguration;
import alluxio.conf.Configuration;
import alluxio.conf.PropertyKey;
import alluxio.fuse.AlluxioFuseUtils;
import alluxio.fuse.AlluxioFuseUtils.CloseableFuseFileInfo;
import alluxio.fuse.AlluxioJniFuseFileSystem;
import alluxio.fuse.options.FuseOptions;
import alluxio.jnifuse.LibFuse;
import alluxio.jnifuse.struct.FuseFileInfo;
import alluxio.util.io.BufferUtils;

import jnr.constants.platform.OpenFlags;
import org.junit.Assert;
import org.junit.Test;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.HashSet;

/**
 * Integration tests for JNR-FUSE based {@link AlluxioJniFuseFileSystem}.
 */
public class JNIFuseIntegrationTest extends AbstractFuseIntegrationTest {
  private static final int FILE_LEN = 128;
  private AlluxioJniFuseFileSystem mFuseFileSystem;

  @Override
  public void configure() {
  }

  @Override
  public void mountFuse(FileSystemContext context,
                        FileSystem fileSystem, String mountPoint, String alluxioRoot) {
    Configuration.set(PropertyKey.FUSE_MOUNT_ALLUXIO_PATH, alluxioRoot);
    Configuration.set(PropertyKey.FUSE_MOUNT_POINT, mountPoint);
    AlluxioConfiguration conf = Configuration.global();
    LibFuse.loadLibrary(AlluxioFuseUtils.getLibfuseVersion(conf));
    mFuseFileSystem = new AlluxioJniFuseFileSystem(context, fileSystem,
        FuseOptions.Builder.fromConfig(conf).build());
    mFuseFileSystem.mount(false, false, new HashSet<>());
  }

  @Override
  public void beforeStop() throws IOException {
    try {
      mFuseFileSystem.umount(true);
    } catch (Exception e) {
      // will try umounting from shell
    }
    umountFromShellIfMounted();
  }

  @Override
  public void afterStop() {
    // noop
  }

  /**
   * Tests creating a file for writing
   * and opening a file for O_RDONLY read-only open flag.
   */
  @Test
  public void createWriteOpenRead() throws Exception {
    String testFile = "/createWriteOpenReadTestFile";
    try (CloseableFuseFileInfo info = new CloseableFuseFileInfo()) {
      FuseFileInfo fuseFileInfo = info.get();

      // cannot open non-existing file for read
      fuseFileInfo.flags.set(OpenFlags.O_RDONLY.intValue());
      Assert.assertNotEquals(0, mFuseFileSystem.open(testFile, fuseFileInfo));

      // open existing file for read
      createTestFile(testFile, fuseFileInfo, FILE_LEN);
      readAndValidateTestFile(testFile, fuseFileInfo, FILE_LEN);
    }
  }

  /**
   * Tests opening a file with O_WRONLY flag on non-existing file.
   */
  @Test
  public void openWriteNonExisting() throws Exception {
    String testFile = "/openWriteNonExisting";
    try (CloseableFuseFileInfo closeableFuseFileInfo = new CloseableFuseFileInfo()) {
      FuseFileInfo info = closeableFuseFileInfo.get();
      info.flags.set(OpenFlags.O_WRONLY.intValue());
      Assert.assertEquals(0, mFuseFileSystem.open(testFile, info));
      ByteBuffer buffer = BufferUtils.getIncreasingByteBuffer(FILE_LEN);
      try {
        Assert.assertEquals(FILE_LEN, mFuseFileSystem.write(testFile, buffer, FILE_LEN, 0, info));
      } finally {
        Assert.assertEquals(0, mFuseFileSystem.release(testFile, info));
      }
      readAndValidateTestFile(testFile, info, FILE_LEN);
    }
  }

  /**
   * Tests opening a file with O_WRONLY on existing file without O_TRUNC.
   */
  @Test
  public void openWriteExistingWithoutTruncFlag() throws Exception {
    String testFile = "/openWriteExistingWithoutTruncFlag";
    try (CloseableFuseFileInfo closeableFuseFileInfo = new CloseableFuseFileInfo()) {
      FuseFileInfo info = closeableFuseFileInfo.get();
      createTestFile(testFile, info, FILE_LEN);

      info.flags.set(OpenFlags.O_WRONLY.intValue());
      try {
        Assert.assertEquals(0, mFuseFileSystem.open(testFile, info));
        ByteBuffer buffer = BufferUtils.getIncreasingByteBuffer(FILE_LEN);
        // O_WRONLY without O_TRUNC cannot overwrite
        Assert.assertTrue(mFuseFileSystem.write(testFile, buffer, FILE_LEN, 0, info) < 0);
      } finally {
        mFuseFileSystem.release(testFile, info);
      }
    }
  }

  /**
   * Tests opening a file with O_WRONLY and O_TRUNC flags to overwrite existing file.
   */
  @Test
  public void openWriteExistingWithTruncFlag() throws Exception {
    String testFile = "/openWriteExistingWithTruncFlag";
    try (CloseableFuseFileInfo closeableFuseFileInfo = new CloseableFuseFileInfo()) {
      FuseFileInfo info = closeableFuseFileInfo.get();
      createTestFile(testFile, info, FILE_LEN / 2);

      info.flags.set(OpenFlags.O_WRONLY.intValue() | OpenFlags.O_TRUNC.intValue());
      try {
        Assert.assertEquals(0, mFuseFileSystem.open(testFile, info));
        ByteBuffer buffer = BufferUtils.getIncreasingByteBuffer(FILE_LEN);
        Assert.assertEquals(FILE_LEN, mFuseFileSystem.write(testFile, buffer, FILE_LEN, 0, info));
      } finally {
        mFuseFileSystem.release(testFile, info);
      }
      readAndValidateTestFile(testFile, info, FILE_LEN);
    }
  }

  /**
   * Tests opening a file with O_WRONLY and then truncating it to zero
   * length for writing.
   */
  @Test
  public void openWriteExistingWithTruncate() throws Exception {
    String testFile = "/openWriteExistingWithTruncate";
    try (CloseableFuseFileInfo closeableFuseFileInfo = new CloseableFuseFileInfo()) {
      FuseFileInfo info = closeableFuseFileInfo.get();
      createTestFile(testFile, info, FILE_LEN / 2);

      info.flags.set(OpenFlags.O_WRONLY.intValue());
      Assert.assertEquals(0, mFuseFileSystem.open(testFile, info));
      try {
        // truncate to original length is no-op
        Assert.assertEquals(0, mFuseFileSystem.truncate(testFile, FILE_LEN / 2));
        // truncate to a large value
        Assert.assertNotEquals(0, mFuseFileSystem.truncate(testFile, FILE_LEN));
        // delete file
        Assert.assertEquals(0, mFuseFileSystem.truncate(testFile, 0));
        Assert.assertEquals(0, mFuseFileSystem.truncate(testFile, FILE_LEN * 2));
        ByteBuffer buffer = BufferUtils.getIncreasingByteBuffer(FILE_LEN);
        Assert.assertEquals(FILE_LEN, mFuseFileSystem.write(testFile, buffer, FILE_LEN, 0, info));
      } finally {
        mFuseFileSystem.release(testFile, info);
      }
      info.flags.set(OpenFlags.O_RDONLY.intValue());
      Assert.assertEquals(0, mFuseFileSystem.open(testFile, info));
      try {
        ByteBuffer buffer = ByteBuffer.wrap(new byte[FILE_LEN]);
        Assert.assertEquals(FILE_LEN, mFuseFileSystem.read(testFile, buffer, FILE_LEN, 0, info));
        Assert.assertTrue(BufferUtils.equalIncreasingByteArray(FILE_LEN, buffer.array()));
        buffer = ByteBuffer.wrap(new byte[FILE_LEN]);
        Assert.assertEquals(FILE_LEN,
            mFuseFileSystem.read(testFile, buffer, FILE_LEN, FILE_LEN, info));
        for (byte cur : buffer.array()) {
          Assert.assertEquals((byte) 0, cur);
        }
      } finally {
        Assert.assertEquals(0, mFuseFileSystem.release(testFile, info));
      }
    }
  }

  /**
   * Tests opening file with O_RDWR on non-existing file for write-only workloads.
   */
  @Test
  public void openReadWriteNonExisting() throws Exception {
    String testFile = "/openReadWriteNonExistingFile";
    try (CloseableFuseFileInfo closeableFuseFileInfo = new CloseableFuseFileInfo()) {
      FuseFileInfo info = closeableFuseFileInfo.get();
      info.flags.set(OpenFlags.O_RDWR.intValue());
      Assert.assertEquals(0, mFuseFileSystem.open(testFile, info));
      try {
        ByteBuffer buffer = BufferUtils.getIncreasingByteBuffer(FILE_LEN);
        Assert.assertEquals(FILE_LEN, mFuseFileSystem.write(testFile, buffer, FILE_LEN, 0, info));
        buffer.clear();
        Assert.assertTrue(mFuseFileSystem.read(testFile, buffer, FILE_LEN, 0, info) < 0);
      } finally {
        Assert.assertEquals(0, mFuseFileSystem.release(testFile, info));
      }
      readAndValidateTestFile(testFile, info, FILE_LEN);
    }
  }

  /**
   * Tests opening file with O_RDWR on existing file for read-only workloads.
   */
  @Test
  public void openReadWriteExisting() throws Exception {
    String testFile = "/openReadWriteExisting";
    try (CloseableFuseFileInfo closeableFuseFileInfo = new CloseableFuseFileInfo()) {
      FuseFileInfo info = closeableFuseFileInfo.get();
      createTestFile(testFile, info, FILE_LEN);

      info.flags.set(OpenFlags.O_RDWR.intValue());
      Assert.assertEquals(0, mFuseFileSystem.open(testFile, info));
      try {
        ByteBuffer buffer = ByteBuffer.wrap(new byte[FILE_LEN]);
        Assert.assertEquals(FILE_LEN, mFuseFileSystem.read(testFile, buffer, FILE_LEN, 0, info));
        Assert.assertTrue(BufferUtils.equalIncreasingByteArray(FILE_LEN, buffer.array()));
        Assert.assertTrue(mFuseFileSystem.write(testFile, buffer, FILE_LEN, 0, info) < 0);
      } finally {
        Assert.assertEquals(0, mFuseFileSystem.release(testFile, info));
      }
    }
  }

  /**
   * Tests opening file with O_RDWR on existing empty for write-only workloads.
   */
  @Test
  public void openReadWriteEmptyFile() throws Exception {
    String testFile = "/openReadWriteEmptyFile";
    try (CloseableFuseFileInfo closeableFuseFileInfo = new CloseableFuseFileInfo()) {
      FuseFileInfo info = closeableFuseFileInfo.get();
      // Create empty file
      info.flags.set(OpenFlags.O_WRONLY.intValue());
      Assert.assertEquals(0, mFuseFileSystem.create(testFile, 100644, info));
      Assert.assertEquals(0, mFuseFileSystem.release(testFile, info));
      // Open empty file for write
      info.flags.set(OpenFlags.O_RDWR.intValue());
      Assert.assertEquals(0, mFuseFileSystem.open(testFile, info));
      try {
        ByteBuffer buffer = BufferUtils.getIncreasingByteBuffer(FILE_LEN);
        Assert.assertEquals(FILE_LEN, mFuseFileSystem.write(testFile, buffer, FILE_LEN, 0, info));
        buffer.clear();
        Assert.assertTrue(mFuseFileSystem.read(testFile, buffer, FILE_LEN, 0, info) < 0);
      } finally {
        Assert.assertEquals(0, mFuseFileSystem.release(testFile, info));
      }
      readAndValidateTestFile(testFile, info, FILE_LEN);
    }
  }

  /**
   * Tests opening file with O_RDWR and O_TRUNC flags on existing file
   * for write-only workloads.
   */
  @Test
  public void openReadWriteTruncExisting() throws Exception {
    String testFile = "/openReadWriteTruncExisting";
    try (CloseableFuseFileInfo closeableFuseFileInfo = new CloseableFuseFileInfo()) {
      FuseFileInfo info = closeableFuseFileInfo.get();
      createTestFile(testFile, info, FILE_LEN / 2);

      info.flags.set(OpenFlags.O_RDWR.intValue() | OpenFlags.O_TRUNC.intValue());
      Assert.assertEquals(0, mFuseFileSystem.open(testFile, info));
      try {
        ByteBuffer buffer = BufferUtils.getIncreasingByteBuffer(FILE_LEN);
        Assert.assertEquals(FILE_LEN, mFuseFileSystem.write(testFile, buffer, FILE_LEN, 0, info));
        buffer.clear();
        Assert.assertTrue(mFuseFileSystem.read(testFile, buffer, FILE_LEN, 0, info) < 0);
      } finally {
        Assert.assertEquals(0, mFuseFileSystem.release(testFile, info));
      }
      readAndValidateTestFile(testFile, info, FILE_LEN);
    }
  }

  private void createTestFile(String testFile, FuseFileInfo info, int fileLen) {
    info.flags.set(OpenFlags.O_WRONLY.intValue());
    Assert.assertEquals(0, mFuseFileSystem.create(testFile, 100644, info));
    ByteBuffer buffer = BufferUtils.getIncreasingByteBuffer(fileLen);
    try {
      Assert.assertEquals(fileLen, mFuseFileSystem.write(testFile, buffer, fileLen, 0, info));
    } finally {
      Assert.assertEquals(0, mFuseFileSystem.release(testFile, info));
    }
  }

  private void readAndValidateTestFile(String testFile, FuseFileInfo info, int fileLen) {
    info.flags.set(OpenFlags.O_RDONLY.intValue());
    Assert.assertEquals(0, mFuseFileSystem.open(testFile, info));
    try {
      ByteBuffer buffer = ByteBuffer.wrap(new byte[fileLen]);
      Assert.assertEquals(fileLen, mFuseFileSystem.read(testFile, buffer, fileLen, 0, info));
      Assert.assertTrue(BufferUtils.equalIncreasingByteArray(fileLen, buffer.array()));
    } finally {
      Assert.assertEquals(0, mFuseFileSystem.release(testFile, info));
    }
  }
}
