/*
 * 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 static org.apache.hadoop.fs.contract.ContractTestUtils.cleanup;
import static org.apache.hadoop.fs.contract.ContractTestUtils.skip;
import static org.assertj.core.api.Assumptions.assumeThat;

import java.io.IOException;
import java.net.URI;
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.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This is the base class for all the contract tests.
 */
public abstract class AbstractFSContractTestBase 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 tests.
   */
  private AbstractFSContract contract;

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

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

  private String testMethodName;

  @BeforeAll
  public static void nameTestThread() {
    Thread.currentThread().setName("JUnit");
  }

  private void nameThread() {
    Thread.currentThread().setName("JUnit-" + getMethodName());
  }

  @BeforeEach
  void getTestMethodName(TestInfo testInfo) {
    testInfo.getTestMethod().ifPresent(m -> testMethodName = m.getName());
    nameThread();
  }

  protected String getMethodName() {
    return testMethodName;
  }

  /**
   * 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 getLogger() {
    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() {
    assumeThat(contract.isEnabled())
        .withFailMessage("test cases disabled for " + contract)
        .isTrue();
  }

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

  /**
   * 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
   */
  @BeforeEach
  public void setup() throws Exception {
    Thread.currentThread().setName("setup");
    LOG.debug("== Setup ==");
    contract = createContract(createConfiguration());
    contract.init();
    //skip tests if they aren't enabled
    assumeEnabled();
    //extract the test FS
    fileSystem = contract.getTestFileSystem();
    Assertions.assertThat(fileSystem)
        .withFailMessage("null filesystem")
        .isNotNull();
    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
    Assertions.assertThat(fsURI.getScheme())
        .withFailMessage("wrong filesystem of " + fsURI)
        .isEqualTo(contract.getScheme());
    //create the test path
    testPath = getContract().getTestPath();
    mkdirs(testPath);
    LOG.debug("== Setup complete ==");
  }

  /**
   * Teardown.
   * @throws Exception on any failure
   */
  @AfterEach
  public void teardown() throws Exception {
    Thread.currentThread().setName("teardown");
    LOG.debug("== Teardown ==");
    deleteTestDirInTeardown();
    if (contract != null) {
      contract.teardown();
    }
    LOG.debug("== Teardown complete ==");
  }

  /**
   * 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));
  }

  /**
   * Get a path whose name ends with the name of this method.
   * @return a path implicitly unique amongst all methods in this class
   * @throws IOException IO problems
   */
  protected Path methodPath() throws IOException {
    return path(getMethodName());
  }

  /**
   * 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) {
    getLogger().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 exist.
   * @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 {
    Assertions.assertThat(fileSystem.mkdirs(path))
        .withFailMessage("Failed to mkdir " + path)
        .isTrue();
  }

  /**
   * 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) {
    Assertions.assertThat(result)
        .withFailMessage(text + " wrong read result " + result)
        .isEqualTo(-1);
  }

  protected 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();
    getLogger().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;
  }

}
