/*
 * 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 org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.internal.AssumptionViolatedException;
import org.junit.rules.Timeout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.URI;

import static org.apache.hadoop.fs.contract.ContractTestUtils.cleanup;
import static org.apache.hadoop.fs.contract.ContractTestUtils.skip;

/**
 * This is the base class for all the contract tests
 */
public abstract class AbstractFSContractTestBase extends Assert
  implements ContractOptions {

  private static final Logger LOG =
    LoggerFactory.getLogger(AbstractFSContractTestBase.class);

  /**
   * Length of files to work with: {@value}
   */
  public static final int TEST_FILE_LEN = 1024;

  /**
   * standard test timeout: {@value}
   */
  public static final int DEFAULT_TEST_TIMEOUT = 180 * 1000;

  /**
   * The FS contract used for these tets
   */
  private AbstractFSContract contract;

  /**
   * The test filesystem extracted from it
   */
  private FileSystem fileSystem;

  /**
   * The path for tests
   */
  private Path testPath;

  /**
   * This must be implemented by all instantiated test cases
   * -provide the FS contract
   * @return the FS contract
   */
  protected abstract AbstractFSContract createContract(Configuration conf);

  /**
   * Get the contract
   * @return the contract, which will be non-null once the setup operation has
   * succeeded
   */
  protected AbstractFSContract getContract() {
    return contract;
  }

  /**
   * Get the filesystem created in startup
   * @return the filesystem to use for tests
   */
  public FileSystem getFileSystem() {
    return fileSystem;
  }

  /**
   * Get the log of the base class
   * @return a logger
   */
  public static Logger getLog() {
    return LOG;
  }

  /**
   * Skip a test if a feature is unsupported in this FS
   * @param feature feature to look for
   * @throws IOException IO problem
   */
  protected void skipIfUnsupported(String feature) throws IOException {
    if (!isSupported(feature)) {
      skip("Skipping as unsupported feature: " + feature);
    }
  }

  /**
   * Is a feature supported?
   * @param feature feature
   * @return true iff the feature is supported
   * @throws IOException IO problems
   */
  protected boolean isSupported(String feature) throws IOException {
    return contract.isSupported(feature, false);
  }

  /**
   * Include at the start of tests to skip them if the FS is not enabled.
   */
  protected void assumeEnabled() {
    if (!contract.isEnabled())
      throw new AssumptionViolatedException("test cases disabled for " + contract);
  }

  /**
   * Create a configuration. May be overridden by tests/instantiations
   * @return a configuration
   */
  protected Configuration createConfiguration() {
    return new Configuration();
  }

  /**
   * Set the timeout for every test
   */
  @Rule
  public Timeout testTimeout = new Timeout(getTestTimeoutMillis());

  /**
   * Option for tests to override the default timeout value
   * @return the current test timeout
   */
  protected int getTestTimeoutMillis() {
    return DEFAULT_TEST_TIMEOUT;
  }


  /**
   * Setup: create the contract then init it
   * @throws Exception on any failure
   */
  @Before
  public void setup() throws Exception {
    contract = createContract(createConfiguration());
    contract.init();
    //skip tests if they aren't enabled
    assumeEnabled();
    //extract the test FS
    fileSystem = contract.getTestFileSystem();
    assertNotNull("null filesystem", fileSystem);
    URI fsURI = fileSystem.getUri();
    LOG.info("Test filesystem = {} implemented by {}",
        fsURI, fileSystem);
    //sanity check to make sure that the test FS picked up really matches
    //the scheme chosen. This is to avoid defaulting back to the localFS
    //which would be drastic for root FS tests
    assertEquals("wrong filesystem of " + fsURI,
                 contract.getScheme(), fsURI.getScheme());
    //create the test path
    testPath = getContract().getTestPath();
    mkdirs(testPath);
  }

  /**
   * Teardown
   * @throws Exception on any failure
   */
  @After
  public void teardown() throws Exception {
    deleteTestDirInTeardown();
  }

  /**
   * Delete the test dir in the per-test teardown
   * @throws IOException
   */
  protected void deleteTestDirInTeardown() throws IOException {
    cleanup("TEARDOWN", getFileSystem(), testPath);
  }

  /**
   * Create a path under the test path provided by
   * the FS contract
   * @param filepath path string in
   * @return a path qualified by the test filesystem
   * @throws IOException IO problems
   */
  protected Path path(String filepath) throws IOException {
    return getFileSystem().makeQualified(
      new Path(getContract().getTestPath(), filepath));
  }

  /**
   * Take a simple path like "/something" and turn it into
   * a qualified path against the test FS
   * @param filepath path string in
   * @return a path qualified by the test filesystem
   * @throws IOException IO problems
   */
  protected Path absolutepath(String filepath) throws IOException {
    return getFileSystem().makeQualified(new Path(filepath));
  }

  /**
   * List a path in the test FS
   * @param path path to list
   * @return the contents of the path/dir
   * @throws IOException IO problems
   */
  protected String ls(Path path) throws IOException {
    return ContractTestUtils.ls(fileSystem, path);
  }

  /**
   * Describe a test. This is a replacement for javadocs
   * where the tests role is printed in the log output
   * @param text description
   */
  protected void describe(String text) {
    LOG.info(text);
  }

  /**
   * Handle the outcome of an operation not being the strictest
   * exception desired, but one that, while still within the boundary
   * of the contract, is a bit looser.
   *
   * If the FS contract says that they support the strictest exceptions,
   * that is what they must return, and the exception here is rethrown
   * @param action Action
   * @param expectedException what was expected
   * @param e exception that was received
   */
  protected void handleRelaxedException(String action,
                                        String expectedException,
                                        Exception e) throws Exception {
    if (getContract().isSupported(SUPPORTS_STRICT_EXCEPTIONS, false)) {
      throw e;
    }
    LOG.warn("The expected exception {}  was not the exception class" +
             " raised on {}: {}", action , e.getClass(), expectedException, e);
  }

  /**
   * Handle expected exceptions through logging and/or other actions
   * @param e exception raised.
   */
  protected void handleExpectedException(Exception e) {
    getLog().debug("expected :{}" ,e, e);
  }

  /**
   * assert that a path exists
   * @param message message to use in an assertion
   * @param path path to probe
   * @throws IOException IO problems
   */
  public void assertPathExists(String message, Path path) throws IOException {
    ContractTestUtils.assertPathExists(fileSystem, message, path);
  }

  /**
   * assert that a path does not
   * @param message message to use in an assertion
   * @param path path to probe
   * @throws IOException IO problems
   */
  public void assertPathDoesNotExist(String message, Path path) throws
                                                                IOException {
    ContractTestUtils.assertPathDoesNotExist(fileSystem, message, path);
  }

  /**
   * Assert that a file exists and whose {@link FileStatus} entry
   * declares that this is a file and not a symlink or directory.
   *
   * @param filename name of the file
   * @throws IOException IO problems during file operations
   */
  protected void assertIsFile(Path filename) throws IOException {
    ContractTestUtils.assertIsFile(fileSystem, filename);
  }

  /**
   * Assert that a file exists and whose {@link FileStatus} entry
   * declares that this is a file and not a symlink or directory.
   *
   * @param path name of the file
   * @throws IOException IO problems during file operations
   */
  protected void assertIsDirectory(Path path) throws IOException {
    ContractTestUtils.assertIsDirectory(fileSystem, path);
  }


  /**
   * Assert that a file exists and whose {@link FileStatus} entry
   * declares that this is a file and not a symlink or directory.
   *
   * @throws IOException IO problems during file operations
   */
  protected void mkdirs(Path path) throws IOException {
    assertTrue("Failed to mkdir " + path, fileSystem.mkdirs(path));
  }

  /**
   * Assert that a delete succeeded
   * @param path path to delete
   * @param recursive recursive flag
   * @throws IOException IO problems
   */
  protected void assertDeleted(Path path, boolean recursive) throws
                                                             IOException {
    ContractTestUtils.assertDeleted(fileSystem, path, recursive);
  }

  /**
   * Assert that the result value == -1; which implies
   * that a read was successful
   * @param text text to include in a message (usually the operation)
   * @param result read result to validate
   */
  protected void assertMinusOne(String text, int result) {
    assertEquals(text + " wrong read result " + result, -1, result);
  }

  boolean rename(Path src, Path dst) throws IOException {
    return getFileSystem().rename(src, dst);
  }

  protected String generateAndLogErrorListing(Path src, Path dst) throws
                                                                  IOException {
    FileSystem fs = getFileSystem();
    getLog().error(
      "src dir " + ContractTestUtils.ls(fs, src.getParent()));
    String destDirLS = ContractTestUtils.ls(fs, dst.getParent());
    if (fs.isDirectory(dst)) {
      //include the dir into the listing
      destDirLS = destDirLS + "\n" + ContractTestUtils.ls(fs, dst);
    }
    return destDirLS;
  }
}
