/*
 * 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.hadoop.fs.contract;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.CommonConfigurationKeysPublic;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.Options.HandleOpt;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.PathHandle;
import org.apache.hadoop.fs.RawPathHandle;
import org.apache.hadoop.io.IOUtils;

import static org.apache.hadoop.fs.contract.ContractTestUtils.appendFile;
import static org.apache.hadoop.fs.contract.ContractTestUtils.createFile;
import static org.apache.hadoop.fs.contract.ContractTestUtils.dataset;
import static org.apache.hadoop.fs.contract.ContractTestUtils.skip;
import static org.apache.hadoop.fs.contract.ContractTestUtils.touch;
import static org.apache.hadoop.fs.contract.ContractTestUtils.verifyRead;
import static org.apache.hadoop.fs.contract.ContractTestUtils.verifyFileContents;

import org.junit.Test;

/**
 * Test Seek operations
 */
public abstract class AbstractContractOpenTest
    extends AbstractFSContractTestBase {

  private FSDataInputStream instream;

  @Override
  protected Configuration createConfiguration() {
    Configuration conf = super.createConfiguration();
    conf.setInt(CommonConfigurationKeysPublic.IO_FILE_BUFFER_SIZE_KEY, 4096);
    return conf;
  }

  @Override
  public void teardown() throws Exception {
    IOUtils.closeStream(instream);
    instream = null;
    super.teardown();
  }

  @Test
  public void testOpenReadZeroByteFile() throws Throwable {
    describe("create & read a 0 byte file");
    Path path = path("zero.txt");
    touch(getFileSystem(), path);
    instream = getFileSystem().open(path);
    assertEquals(0, instream.getPos());
    //expect initial read to fail
    int result = instream.read();
    assertMinusOne("initial byte read", result);
  }

  @Test
  public void testFsIsEncrypted() throws Exception {
      describe("create an empty file and call FileStatus.isEncrypted()");
      final Path path = path("file");
      createFile(getFileSystem(), path, false, new byte[0]);
      final FileStatus stat = getFileSystem().getFileStatus(path);
      assertFalse("Expecting false for stat.isEncrypted()",
          stat.isEncrypted());
  }

  @Test
  public void testOpenReadDir() throws Throwable {
    describe("create & read a directory");
    Path path = path("zero.dir");
    mkdirs(path);
    try {
      instream = getFileSystem().open(path);
      //at this point we've opened a directory
      fail("A directory has been opened for reading");
    } catch (FileNotFoundException e) {
      handleExpectedException(e);
    } catch (IOException e) {
      handleRelaxedException("opening a directory for reading",
                             "FileNotFoundException",
                             e);
    }
  }

  @Test
  public void testOpenReadDirWithChild() throws Throwable {
    describe("create & read a directory which has a child");
    Path path = path("zero.dir");
    mkdirs(path);
    Path path2 = new Path(path, "child");
    mkdirs(path2);

    try {
      instream = getFileSystem().open(path);
      //at this point we've opened a directory
      fail("A directory has been opened for reading");
    } catch (FileNotFoundException e) {
      handleExpectedException(e);
    } catch (IOException e) {
      handleRelaxedException("opening a directory for reading",
                             "FileNotFoundException",
                             e);
    }
  }

  @Test
  public void testOpenFileTwice() throws Throwable {
    describe("verify that two opened file streams are independent");
    Path path = path("testopenfiletwice.txt");
    byte[] block = dataset(TEST_FILE_LEN, 0, 255);
    //this file now has a simple rule: offset => value
    createFile(getFileSystem(), path, true, block);
    //open first
    FSDataInputStream instream1 = getFileSystem().open(path);
    FSDataInputStream instream2 = null;
    try {
      int c = instream1.read();
      assertEquals(0,c);
      instream2 = getFileSystem().open(path);
      assertEquals("first read of instream 2", 0, instream2.read());
      assertEquals("second read of instream 1", 1, instream1.read());
      instream1.close();
      assertEquals("second read of instream 2", 1, instream2.read());
      //close instream1 again
      instream1.close();
    } finally {
      IOUtils.closeStream(instream1);
      IOUtils.closeStream(instream2);
    }
  }

  @Test
  public void testSequentialRead() throws Throwable {
    describe("verify that sequential read() operations return values");
    Path path = path("testsequentialread.txt");
    int len = 4;
    int base = 0x40; // 64
    byte[] block = dataset(len, base, base + len);
    //this file now has a simple rule: offset => (value | 0x40)
    createFile(getFileSystem(), path, true, block);
    //open first
    instream = getFileSystem().open(path);
    assertEquals(base, instream.read());
    assertEquals(base + 1, instream.read());
    assertEquals(base + 2, instream.read());
    assertEquals(base + 3, instream.read());
    // and now, failures
    assertEquals(-1, instream.read());
    assertEquals(-1, instream.read());
    instream.close();
  }

  /**
   * Skip a test case if the FS doesn't support file references.
   * The feature is assumed to be unsupported unless stated otherwise.
   */
  protected void assumeSupportsFileReference() throws IOException {
    if (getContract().isSupported(SUPPORTS_FILE_REFERENCE, false)) {
      return;
    }
    skip("Skipping as unsupported feature: " + SUPPORTS_FILE_REFERENCE);
  }

  /**
   * Skip a test case if the FS doesn't support content validation.
   * The feature is assumed to be unsupported unless stated otherwise.
   */
  protected void assumeSupportsContentCheck() throws IOException {
    if (getContract().isSupported(SUPPORTS_CONTENT_CHECK, false)) {
      return;
    }
    skip("Skipping as unsupported feature: " + SUPPORTS_CONTENT_CHECK);
  }

  private PathHandle getHandleOrSkip(FileStatus stat, HandleOpt... opts) {
    try {
      return getFileSystem().getPathHandle(stat, opts);
    } catch (UnsupportedOperationException e) {
      skip("FileSystem does not support " + Arrays.toString(opts));
    }
    // unreachable
    return null;
  }

  /**
   * Verify {@link HandleOpt#exact()} handle semantics.
   * @throws Throwable on error
   */
  @Test
  public void testOpenFileByExact() throws Throwable {
    describe("verify open(getPathHandle(FileStatus, exact())) operations" +
        "detect changes");
    assumeSupportsContentCheck();
    assumeSupportsFileReference();
    Path path1 = path("testopenfilebyexact1");
    Path path2 = path("testopenfilebyexact2");
    byte[] file1 = dataset(TEST_FILE_LEN, 43, 255);
    createFile(getFileSystem(), path1, false, file1);
    FileStatus stat = getFileSystem().getFileStatus(path1);
    assertNotNull(stat);
    assertEquals(path1, stat.getPath());
    ContractTestUtils.rename(getFileSystem(), path1, path2);
    // create identical file at same location, orig still exists at path2
    createFile(getFileSystem(), path1, false, file1);

    PathHandle fd = getHandleOrSkip(stat, HandleOpt.exact());

    // verify path1, path2 contents identical
    verifyFileContents(getFileSystem(), path1, file1);
    verifyFileContents(getFileSystem(), path2, file1);
    try {
      // the PathHandle will not resolve, even though
      // the original entity exists, it has not been modified, and an
      // identical file exists at the old path. The handle would also
      // fail to resolve if path1 had been modified
      instream = getFileSystem().open(fd, 1 << 15);
      fail("Expected an exception");
    } catch (IOException e) {
      // expected
    }
  }

  /**
   * Verify {@link HandleOpt#content()} handle semantics.
   * @throws Throwable on error
   */
  @Test
  public void testOpenFileByContent() throws Throwable {
    describe("verify open(getPathHandle(FileStatus, content())) operations" +
        "follow relocation");
    assumeSupportsContentCheck();
    assumeSupportsFileReference();
    Path path1 = path("testopenfilebycontent1");
    Path path2 = path("testopenfilebycontent2");
    byte[] file1 = dataset(TEST_FILE_LEN, 43, 255);
    createFile(getFileSystem(), path1, false, file1);
    FileStatus stat = getFileSystem().getFileStatus(path1);
    assertNotNull(stat);
    assertEquals(path1, stat.getPath());
    // rename the file after obtaining FileStatus
    ContractTestUtils.rename(getFileSystem(), path1, path2);

    // obtain handle to entity from #getFileStatus call
    PathHandle fd = getHandleOrSkip(stat, HandleOpt.content());

    try (FSDataInputStream in = getFileSystem().open(fd, 1 << 15)) {
      // verify read of consistent content at new location
      verifyRead(in, file1, 0, TEST_FILE_LEN);
    }

    // modify the file at its new location by appending data
    byte[] file1a = dataset(TEST_FILE_LEN, 44, 255);
    appendFile(getFileSystem(), path2, file1a);
    byte[] file1x = Arrays.copyOf(file1, file1.length + file1a.length);
    System.arraycopy(file1a, 0, file1x, file1.length, file1a.length);
    // verify fd entity contains contents of file1 + appended bytes
    verifyFileContents(getFileSystem(), path2, file1x);

    try {
      // handle should not resolve when content changed
      instream = getFileSystem().open(fd, 1 << 15);
      fail("Failed to detect change to content");
    } catch (IOException e) {
      // expected
    }
  }


  /**
   * Verify {@link HandleOpt#path()} handle semantics.
   * @throws Throwable on error
   */
  @Test
  public void testOpenFileByPath() throws Throwable {
    describe("verify open(getPathHandle(FileStatus, path())) operations" +
        "detect changes");
    assumeSupportsContentCheck();
    Path path1 = path("testopenfilebypath1");
    Path path2 = path("testopenfilebypath2");

    byte[] file1 = dataset(TEST_FILE_LEN, 43, 255);
    createFile(getFileSystem(), path1, false, file1);
    FileStatus stat = getFileSystem().getFileStatus(path1);
    assertNotNull(stat);
    assertEquals(path1, stat.getPath());
    ContractTestUtils.rename(getFileSystem(), path1, path2);
    // create identical file at same location, orig still exists at path2
    createFile(getFileSystem(), path1, false, file1);

    PathHandle fd = getHandleOrSkip(stat, HandleOpt.path());

    // verify path1, path2 contents identical
    verifyFileContents(getFileSystem(), path1, file1);
    verifyFileContents(getFileSystem(), path2, file1);
    try {
      // verify attempt to resolve the handle fails
      instream = getFileSystem().open(fd, 1 << 15);
      fail("Expected an exception");
    } catch (IOException e) {
      // expected
    }
  }

  /**
   * Verify {@link HandleOpt#reference()} handle semantics.
   * @throws Throwable on error
   */
  @Test
  public void testOpenFileByReference() throws Throwable {
    describe("verify open(getPathHandle(FileStatus, reference())) operations" +
        " are independent of rename");
    assumeSupportsFileReference();
    Path path1 = path("testopenfilebyref1");
    Path path2 = path("testopenfilebyref2");

    byte[] file1 = dataset(TEST_FILE_LEN, 43, 255);
    createFile(getFileSystem(), path1, false, file1);
    FileStatus stat = getFileSystem().getFileStatus(path1);
    assertNotNull(stat);
    assertEquals(path1, stat.getPath());
    ContractTestUtils.rename(getFileSystem(), path1, path2);

    byte[] file2 = dataset(TEST_FILE_LEN, 44, 255);
    createFile(getFileSystem(), path1, false, file2);
    byte[] file1a = dataset(TEST_FILE_LEN, 42, 255);
    appendFile(getFileSystem(), path2, file1a);
    byte[] file1x = Arrays.copyOf(file1, file1.length + file1a.length);
    System.arraycopy(file1a, 0, file1x, file1.length, file1a.length);

    PathHandle fd = getHandleOrSkip(stat, HandleOpt.reference());

    // verify path2 contains contents of file1 + appended bytes
    verifyFileContents(getFileSystem(), path2, file1x);
    // verify path1 contents contents of file2
    verifyFileContents(getFileSystem(), path1, file2);

    // verify fd contains contents of file1 + appended bytes
    instream = getFileSystem().open(fd, 1 << 15);
    verifyRead(instream, file1x, 0, TEST_FILE_LEN);
  }

  /**
   * Verify {@link PathHandle} may be serialized and restored.
   * @throws Throwable on error
   */
  @Test
  public void testOpenFileBySerializedReference() throws Throwable {
    describe("verify PathHandle supports generic serialization");
    assumeSupportsFileReference();
    Path path1 = path("testopenfilebyref1");
    Path path2 = path("testopenfilebyref2");

    byte[] file1 = dataset(TEST_FILE_LEN, 43, 255);
    createFile(getFileSystem(), path1, false, file1);
    FileStatus stat = getFileSystem().getFileStatus(path1);
    assertNotNull(stat);
    assertEquals(path1, stat.getPath());
    ContractTestUtils.rename(getFileSystem(), path1, path2);

    byte[] file2 = dataset(TEST_FILE_LEN, 44, 255);
    createFile(getFileSystem(), path1, false, file2);

    PathHandle fd = getHandleOrSkip(stat, HandleOpt.reference());

    // serialize PathHandle
    ByteBuffer sb = fd.bytes();
    PathHandle fdb = new RawPathHandle(sb);

    instream = getFileSystem().open(fdb, 1 << 15);
    // verify stat contains contents of file1
    verifyRead(instream, file1, 0, TEST_FILE_LEN);
    // verify path2 contains contents of file1
    verifyFileContents(getFileSystem(), path2, file1);
    // verify path1 contents contents of file2
    verifyFileContents(getFileSystem(), path1, file2);
  }

}
