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

import org.apache.commons.io.FileUtils;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;

import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;

/**
 * <p>
 * This class is used to create consistently named and safe temp directories for unit tests.
 * </p>
 *
 * <p>
 * A {@link DirTestWatcher} is added to a test by declaring it as a JUnit {@link org.junit.Rule}. A {@link org.junit.Rule Rule} is
 * a piece of code that is run before and after every JUnit test marked with the {@link org.junit.Test Test} annotation. When the
 * {@link DirTestWatcher} is added to a test class the {@link DirTestWatcher} will create a temp directory before each of your
 * {@link org.junit.Test Test}s and optionally delete the temp directory after each of your {@link org.junit.Test Test}s. The <b>base temp directory</b>
 * created by the {@link DirTestWatcher} is in the <b>target</b> folder of the maven project and has the form
 * <b>(my test class fully qualified name)/(my test method name)</b>. So in the context of the code example below, the temp directory created for
 * each test in <b>target</b> will be <b>my.proj.MyTestClass/myTestMethod1</b> and <b>my.proj.MyTestClass/myTestMethod2</b> respectively.
 * </p>
 *
 * <p>
 * The temp directory created by the {@link DirTestWatcher} can be used within a test by simply calling the {@link DirTestWatcher#getDir()}
 * method on the {@link DirTestWatcher} within your unit test.
 * </p>
 *
 * <p>
 * By default, the {@link DirTestWatcher} deletes the temp directory it creates at the end of each {@link org.junit.Test Test}. However, you can create a {@link DirTestWatcher}
 * by doing {@code new DirTestWatcher(false)} to disable the deletion of temp directories after a test. This is useful if you want to examine files after a test runs.
 * </p>
 *
 * <pre>
 * package my.proj;
 *
 * public class MyTestClass {
 *   &#064;Rule
 *   public final DirTestWatcher dirTestWatcher = new DirTestWatcher();
 *
 *   &#064;Test
 *   public void myTestMethod1() {
 *     File dir = dirTestWatcher.getDir();
 *     // Do stuff in the temp directory
 *   }
 *
 *   &#064;Test
 *   public void myTestMethod2() {
 *     File dir = dirTestWatcher.getDir();
 *     // Do stuff in the temp directory
 *   }
 * }
 * </pre>
 *
 * <p>
 * <b>Note:</b> In the code sample above, the directories returned by {@link DirTestWatcher#getDir()} in myTestMethod1 and myTestMethod2 are
 * <b>my.proj.MyTestClass/myTestMethod1</b> and <b>my.proj.MyTestClass/myTestMethod2</b> respectively.
 * </p>
 */
public class DirTestWatcher extends TestWatcher {
  private static final int TEMP_DIR_ATTEMPTS = 10000;

  private String dirPath;
  private File dir;
  private boolean deleteDirAtEnd = true;

  /**
   * Creates a {@link DirTestWatcher} that deletes temp directories after the {@link TestWatcher} completes.
   */
  public DirTestWatcher() {
  }

  /**
   * Creates a {@link DirTestWatcher} which can delete or keep the temp directory after the {@link TestWatcher} completes.
   * @param deleteDirAtEnd When true the temp directory created by the {@link DirTestWatcher} is deleted. When false the
   *                       temp directory created by the {@link DirTestWatcher} is not deleted.
   */
  public DirTestWatcher(boolean deleteDirAtEnd) {
    this.deleteDirAtEnd = deleteDirAtEnd;
  }

  @Override
  protected void starting(Description description) {
    if (description.getMethodName() != null) {
      dirPath = Paths.get(".","target", description.getClassName(), description.getMethodName()).toString();
    } else {
      dirPath = Paths.get(".","target", description.getClassName()).toString();
    }

    dir = new File(dirPath);
    FileUtils.deleteQuietly(dir);
    dir.mkdirs();
  }

  @Override
  protected void finished(Description description) {
    deleteDir();
  }

  @Override
  protected void failed(Throwable e, Description description) {
    deleteDir();
  }

  /**
   * Creates a sub directory with the given relative path in current temp directory
   * @param relativeDirPath The relative path of the sub directory to create in the current temp directory.
   * @return The {@link java.io.File} object of the newly created sub directory.
   */
  public File makeSubDir(Path relativeDirPath) {
    File subDir = dir
      .toPath()
      .resolve(relativeDirPath)
      .toFile();
    subDir.mkdirs();
    return subDir;
  }

  private void deleteDir() {
    if (deleteDirAtEnd) {
      FileUtils.deleteQuietly(dir);
    }
  }

  /**
   * Gets the {@link java.io.File} object of the current temp directory.
   * @return The {@link java.io.File} object of the current temp directory.
   */
  public File getDir() {
    return dir;
  }

  public static File createTempDir(File baseDir) {
    String baseName = System.currentTimeMillis() + "-";

    for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) {
      File tempDir = new File(baseDir, baseName + counter);
      if (tempDir.mkdirs()) {
        return tempDir;
      }
    }

    String message = String.format("Failed to create directory within %s attempts (tried %s0 to %s)",
      TEMP_DIR_ATTEMPTS, baseName, baseName + (TEMP_DIR_ATTEMPTS - 1));
    throw new IllegalStateException(message);
  }
}
