/*
 * Copyright (C) 2022 The Dagger Authors.
 *
 * Licensed 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 dagger.testing.golden;

import androidx.room.compiler.processing.util.Source;
import com.google.common.io.Resources;
import com.google.testing.compile.JavaFileObjects;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.tools.JavaFileObject;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;


/** A test rule that manages golden files for tests. */
public final class GoldenFileRule implements TestRule {
  /** The generated import used in the golden files */
  private static final String GOLDEN_GENERATED_IMPORT =
      "import javax.annotation.processing.Generated;";

  /** The generated import used with the current jdk version */
  private static final String JDK_GENERATED_IMPORT =
      isBeforeJava9()
          ? "import javax.annotation.Generated;"
          : "import javax.annotation.processing.Generated;";

  private static boolean isBeforeJava9() {
    try {
      Class.forName("java.lang.Module");
      return false;
    } catch (ClassNotFoundException e) {
      return true;
    }
  }

  // Parameterized arguments in junit4 are added in brackets to the end of test methods, e.g.
  // `myTestMethod[testParam1=FOO,testParam2=BAR]`. This pattern captures theses into two separate
  // groups, `<GROUP1>[<GROUP2>]` to make it easier when generating the golden file name.
  private static final Pattern JUNIT_PARAMETERIZED_METHOD = Pattern.compile("(.*?)\\[(.*?)\\]");

  private Description description;

  @Override
  public Statement apply(Statement base, Description description) {
    this.description = description;
    return base;
  }

  /**
   * Returns the golden file as a {@link Source} containing the file's content.
   *
   * <p>If the golden file does not exist, the returned file object contains an error message
   * pointing to the location of the missing golden file. This can be used with scripting tools to
   * output the correct golden file in the proper location.
   */
  public Source goldenSource(String generatedFilePath) {
    // Note: we wrap the IOException in a RuntimeException so that this can be called from within
    // the lambda required by XProcessing's testing APIs. We could avoid this by calling this method
    // outside of the lambda, but that seems like an non-worthwile hit to readability.
    try {
      return Source.Companion.java(
          generatedFilePath, goldenFileContent(generatedFilePath.replace('/', '.')));
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Returns the golden file as a {@link JavaFileObject} containing the file's content.
   *
   * If the golden file does not exist, the returned file object contain an error message pointing
   * to the location of the missing golden file. This can be used with scripting tools to output
   * the correct golden file in the proper location.
   */
  public JavaFileObject goldenFile(String qualifiedName) throws IOException {
    return JavaFileObjects.forSourceLines(qualifiedName, goldenFileContent(qualifiedName));
  }

  /**
   * Returns the golden file content.
   *
   * If the golden file does not exist, the returned content contains an error message pointing
   * to the location of the missing golden file. This can be used with scripting tools to output
   * the correct golden file in the proper location.
   */
  public String goldenFileContent(String qualifiedName) throws IOException {
    String fileName =
        String.format(
            "%s_%s_%s",
            description.getTestClass().getSimpleName(),
            getFormattedMethodName(description),
            qualifiedName);

    URL url = description.getTestClass().getResource("goldens/" + fileName);
    return url == null
        // If the golden file does not exist, create a fake file with a comment pointing to the
        // missing golden file. This is helpful for scripts that need to generate golden files from
        // the test failures.
        ? "// Error: Missing golden file for goldens/" + fileName
        // The goldens are generated using jdk 11, so we use this replacement to allow the
        // goldens to also work when compiling using jdk < 9.
        : Resources.toString(url, StandardCharsets.UTF_8)
            .replace(GOLDEN_GENERATED_IMPORT, JDK_GENERATED_IMPORT);
  }

  /**
   * Returns the formatted method name for the given description.
   *
   * <p>If this is not a parameterized test, we return the method name as is. If it is a
   * parameterized test, we format it from {@code someTestMethod[PARAMETER]} to
   * {@code someTestMethod_PARAMETER} to avoid brackets in the name.
   */
  private static String getFormattedMethodName(Description description) {
    Matcher matcher = JUNIT_PARAMETERIZED_METHOD.matcher(description.getMethodName());

    // If this is a parameterized method, separate the parameters with an underscore
    return matcher.find() ? matcher.group(1) + "_" + matcher.group(2) : description.getMethodName();
  }
}
