package com.google.javascript.gents;

import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.collect.Lists;
import com.google.common.io.Files;
import com.google.javascript.clutz.DeclarationGeneratorTest;
import com.google.javascript.gents.TestUtil.TestInput;
import com.google.javascript.gents.TypeScriptGenerator.GentsResult;
import com.google.javascript.gents.experimental.ExperimentTracker;
import com.google.javascript.jscomp.SourceFile;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

@RunWith(Parameterized.class)
public class TypeScriptGeneratorTest {

  private static class SingleTestConfig {
    private final ExperimentTracker experimentTracker;
    private final TestInput testInput;

    SingleTestConfig(ExperimentTracker experimentTracker, TestInput testInput) {
      this.experimentTracker = experimentTracker;
      this.testInput = testInput;
    }

    /**
     * The result from this method is displayed as the text describing a test. This is overridden to
     * express the input file being transformed as well whether the test does or does not have
     * experiments enabled.
     */
    @Override
    public String toString() {
      String result = testInput.getRelativePath();
      if (experimentTracker.hasEnabledExperiments()) {
        return result + " (experimental)";
      }
      return result;
    }
  }

  static final String singleTestPath = "singleTests";

  static final String TEST_EXTERNS_MAP =
      TypeScriptGeneratorTest.getTestDirPath("test_externs_map.json").toString();

  @Parameters(name = "Test {index}: {0}")
  public static Iterable<SingleTestConfig> testCases() throws IOException {
    List<SingleTestConfig> configs = Lists.newArrayList();
    List<File> testInputFiles =
        getTestInputFiles(DeclarationGeneratorTest.JS_NO_EXTERNS_OR_ZIP, singleTestPath);
    File inputFileRoot = getTestDirPath(singleTestPath).toFile().getCanonicalFile();
    for (File file : testInputFiles) {
      TestInput input =
          new TestInput(
              inputFileRoot.getAbsolutePath(), TestUtil.getRelativePathTo(file, inputFileRoot));

      configs.add(new SingleTestConfig(ExperimentTracker.withoutExperiments(), input));

      /*
       * To specify that another experiment should be used, add the Experiment enum
       * value for that experiment in the withExperiments() method.
       */
      // To enable testing an experiment, replace SOME_EXPERIMENT with the experiment
      // enum value below and uncomment the next lines.
      // configs.add(
      //     new SingleTestConfig(
      //         ExperimentTracker.withExperiments(Experiment.SOME_EXPERIMENT), input));
    }
    return configs;
  }

  static List<File> getTestInputFiles(FilenameFilter filter, String... dir) {
    File baseDir = getTestDirPath(dir).toAbsolutePath().toFile();

    File[] testFiles = baseDir.listFiles(filter);
    List<File> filesList = Lists.newArrayList(testFiles);

    return DeclarationGeneratorTest.expandZipTestInputFiles(filesList);
  }

  static Path getTestDirPath(String... testDir) {
    Path p = getPackagePath();
    for (String dir : testDir) {
      p = p.resolve(dir);
    }
    return p;
  }

  static final String SOURCE_ROOT = "";

  static Path getPackagePath() {
    Path root = FileSystems.getDefault().getPath(SOURCE_ROOT);
    Path testDir = root.resolve("src").resolve("test").resolve("java");
    String packageName = TypeScriptGeneratorTest.class.getPackage().getName();
    return testDir.resolve(packageName.replace('.', File.separatorChar));
  }

  static String getFileText(final File input) throws IOException {
    // SourceFile handles <zipfile>!<jsfile> paths internally.
    return SourceFile.fromFile(input.getPath(), UTF_8).getCode();
  }

  private final SingleTestConfig config;

  public TypeScriptGeneratorTest(SingleTestConfig config) {
    this.config = config;
  }

  @Test
  public void runTest() throws Exception {
    File input = config.testInput.getInputFile();
    Options options;
    if (input.getName().equals("externs_map.js")) {
      options = new Options(TypeScriptGeneratorTest.TEST_EXTERNS_MAP);
    } else if (input.getName().equals("externs_override.js")) {
      options =
          new Options(
              TypeScriptGeneratorTest.TEST_EXTERNS_MAP,
              Lists.newArrayList("any:AnyDuringTs37Migration"));
    } else {
      options = new Options();
    }

    TypeScriptGenerator gents = new TypeScriptGenerator(options, config.experimentTracker);

    String basename = gents.pathUtil.getFilePathWithoutExtension(input.getName());
    String sourceText = getFileText(input);
    File goldenFile = config.testInput.getGoldenFile(config.experimentTracker);

    // * The 'empty' test does not need a goog.module declaration because it
    //   is for verifying a file that contains no contents
    // * The 'js_doc_comments" test does not need a goog.module declaration
    //   because it is for verifying a file that contains only JSDoc comments.
    if (!basename.equals("empty") && !basename.equals("js_doc_comments")) {
      TestUtil.assertIsGoogModuleOrProvide(sourceText, basename);
    }

    ByteArrayOutputStream errStream = new ByteArrayOutputStream();
    gents.setErrorStream(new PrintStream(errStream));

    GentsResult gentsResult =
        gents.generateTypeScript(
            Collections.singleton(input.getName()),
            Collections.singletonList(SourceFile.fromCode(input.getName(), sourceText)),
            Collections.emptyList());
    Map<String, String> transpiledSource = gentsResult.sourceFileMap;

    String errors = new String(errStream.toByteArray(), StandardCharsets.UTF_8);
    assertThat(errors).isEmpty();
    assertThat(gents.hasErrors()).isFalse();

    assertThat(transpiledSource).hasSize(1);
    assertThat(transpiledSource).containsKey(basename);

    String transpiledOutput = transpiledSource.get(basename);

    // If the test was run with the UPDATE_GOLDENS environment variable
    // set then the golden file should be updated.
    if (System.getenv("UPDATE_GOLDENS") != null) {
      File parent = goldenFile.getParentFile();
      if (parent != null && !parent.exists()) {
        parent.mkdirs();
      }
      Files.asCharSink(goldenFile, StandardCharsets.UTF_8).write(transpiledOutput);
    }

    String goldenText = getFileText(goldenFile);
    assertThat(transpiledOutput).isEqualTo(goldenText);
  }
}