/*
 * Copyright 2019, Gerwin Klein, Régis Décamps, Steve Rowe
 * SPDX-License-Identifier: BSD-3-Clause
 */

package jflex.maven.plugin.testsuite;

import com.google.common.collect.ImmutableList;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import org.apache.maven.plugin.MojoFailureException;

public class TestCase {

  /** command line switches for jflex invocation */
  private List<String> jflexCmdln;

  /** files on which to invoke jflex */
  private List<String> jflexFiles;

  /** command line switches for javac invocation, instead of the default {@code <testname>.java}. */
  private List<String> javacFiles;

  /** lines with expected differences in jflex output */
  private List<Integer> jflexDiff;

  /** Test input file encoding -- defaults to UTF-8 */
  private String inputFileEncoding = "UTF-8";

  /** Test output file encoding -- defaults to UTF-8 */
  private String outputFileEncoding = "UTF-8";

  /** Single common input file for all outputs */
  private String commonInputFile = null;

  /** Java version this test case is intended for */
  private String javaVersion = System.getProperty("java.version");

  /** misc. */
  private String className;

  private String testName;
  private String description;

  /** base directory of this test case */
  private File testPath;

  private boolean expectJavacFail, expectJFlexFail;

  /** inputOutputFiles to invoke test.main on and compare */
  private List<InputOutput> inputOutput;

  /** encoding to use for compiling the .java file; defaults to UTF-8 */
  private String javacEncoding = "UTF-8";

  /** get- set- methods */
  void setTestName(String s) {
    testName = s;
    // TODO(regisd): The class name should depend on the flex `%class`, not on the test name.
    className = testName.substring(0, 1).toUpperCase(Locale.ENGLISH) + testName.substring(1);
  }

  void setJFlexDiff(List<Integer> d) {
    jflexDiff = d;
  }

  void setDescription(String s) {
    description = s;
  }

  void setExpectJavacFail(boolean b) {
    expectJavacFail = b;
  }

  void setExpectJFlexFail(boolean b) {
    expectJFlexFail = b;
  }

  void setJflexCmdln(List<String> v) {
    jflexCmdln = v;
  }

  void setJavacFiles(List<String> v) {
    javacFiles = v;
  }

  private void setInputOutput(List<InputOutput> v) {
    inputOutput = v;
  }

  void setInputFileEncoding(String e) {
    inputFileEncoding = e;
  }

  void setOutputFileEncoding(String e) {
    outputFileEncoding = e;
  }

  void setCommonInputFile(String f) {
    commonInputFile = f;
  }

  void setJavaVersion(String v) {
    this.javaVersion = v;
  }

  void setJavacEncoding(String v) {
    this.javacEncoding = v;
  }

  public TestCase() {
    jflexFiles = new ArrayList<>();
  }

  boolean checkJavaVersion() {
    String current = System.getProperty("java.version");
    return current.startsWith(javaVersion);
  }

  void init(File testDir) {
    // get files to process
    List<InputOutput> temp = new ArrayList<>();
    String name;
    for (String file : testDir.list()) {
      if (null != commonInputFile) {
        if (Objects.equals(file, testName + ".output")) {
          temp.add(new InputOutput((new File(testDir, testName)).toString(), true));
          commonInputFile = (new File(testDir, commonInputFile)).toString();
        }
      } else {
        if (file.endsWith(".input") && file.startsWith(testName + "-")) {
          name = file.substring(0, file.length() - 6);
          temp.add(
              new InputOutput(
                  (new File(testDir, name)).toString(),
                  new File(testDir, name + ".output").exists()));
        }
      }
    }
    // When a common input file is specified, but no output file exists, add
    // a single InputOutput for the common input file.
    if (null != commonInputFile && 0 == temp.size()) {
      temp.add(new InputOutput(null, false));
    }
    setInputOutput(temp);
    testPath = testDir;
  }

  void createScanner(File jflexUberJar, boolean verbose)
      throws TestFailException, MojoFailureException {
    File lexFile = new File(testPath, testName + ".flex");
    if (verbose) {
      System.out.println(String.format("Open lex specification [%s]", lexFile));
    }
    if (!lexFile.exists()) {
      throw new MojoFailureException(
          "Cannot open lex definition", new FileNotFoundException(lexFile.getPath()));
    }
    jflexFiles.add(lexFile.getPath());
    // invoke JFlex
    TestResult jflexResult = ExecUtils.execJFlex(jflexCmdln, jflexFiles);
    // System.out.println(jflexResult);

    if (jflexResult.getSuccess()) {
      // Scanner generation successful
      if (TestsuiteUtils.verbose) {
        System.out.println("Scanner generation successful");
      }

      if (expectJFlexFail) {
        System.out.println("JFlex failure expected!");
        throw new TestFailException();
      }

      // check JFlex output conformance
      File expected = new File(testPath, testName + "-flex.output");

      if (expected.exists()) {
        DiffStream check = new DiffStream();
        String diff;
        try {
          diff =
              check.diff(
                  jflexDiff, new StringReader(jflexResult.getOutput()), new FileReader(expected));
        } catch (FileNotFoundException e) {
          System.out.println("Error opening file " + expected);
          throw new TestFailException();
        }
        if (diff != null) {
          System.out.println("Test failed, unexpected jflex output: " + diff);
          System.out.println("JFlex output: " + jflexResult.getOutput());
          throw new TestFailException();
        }
      } else {
        System.out.println("Warning: no file for expected output [" + expected + "]");
      }

      // Compile Scanner
      final List<String> toCompile = getFilesToCompile();
      if (TestsuiteUtils.verbose) {
        System.out.println("File(s) to compile: " + toCompile);
      }
      try {
        TestResult javacResult =
            ExecUtils.execJavac(toCompile, testPath, jflexUberJar.getAbsolutePath(), javacEncoding);

        // System.out.println(javacResult);
        if (TestsuiteUtils.verbose) {
          System.out.println(
              "Compilation successful: "
                  + javacResult.getSuccess()
                  + " [expected: "
                  + !expectJavacFail
                  + "]");
        }
        if (javacResult.getSuccess() == expectJavacFail) {
          throw new TestFailException(
              "Compilation failed in " + testPath + " for " + toCompile,
              new Exception(javacResult.getOutput()));
        }
      } catch (FileNotFoundException e) {
        throw new TestFailException("javac: file not found: ", e);
      }
    } else {
      if (!expectJFlexFail) {
        System.out.println("Scanner generation failed!");
        System.out.println("JFlex output was:\n" + jflexResult.getOutput());
        throw new TestFailException();
      } else {
        // check JFlex output conformance
        File expected = new File(testPath, testName + "-flex.output");

        if (expected.exists()) {
          DiffStream check = new DiffStream();
          String diff;
          try {
            diff =
                check.diff(
                    jflexDiff, new StringReader(jflexResult.getOutput()), new FileReader(expected));
          } catch (FileNotFoundException e) {
            System.out.println("Error opening file " + expected);
            throw new TestFailException();
          }
          if (diff != null) {
            System.out.println("Test failed, unexpected jflex output: " + diff);
            System.out.println("JFlex output: " + jflexResult.getOutput());
            throw new TestFailException();
          }
        }
      }
    }
  }

  /** Returns the list of java files to compile. */
  private List<String> getFilesToCompile() {
    if (javacFiles == null) {
      return ImmutableList.of(new File(testPath, className + ".java").getName());
    } else {
      ImmutableList.Builder<String> builder = ImmutableList.builder();
      for (String explicitJavaSrc : javacFiles) {
        File f = new File(testPath, explicitJavaSrc);
        builder.add(f.getName());
      }
      return builder.build();
    }
  }

  boolean hasMoreToDo() {
    // check if there's more files to run scanner main on
    return !(inputOutput.isEmpty());
  }

  void runNext(File jflexUberJar) throws TestFailException, UnsupportedEncodingException {
    // Get first file and remove it from list
    InputOutput current = inputOutput.remove(0);
    // Create List with only first input in
    List<String> inputFiles = new ArrayList<>();
    inputFiles.add(null != commonInputFile ? commonInputFile : current.getName() + ".input");
    // Excute Tester on that input
    List<String> cmdLine = new ArrayList<>();
    cmdLine.add("--encoding");
    cmdLine.add(inputFileEncoding);
    List<File> additionalJars = ImmutableList.of(jflexUberJar);
    TestResult classExecResult =
        ExecUtils.execClass(
            className,
            testPath.toString(),
            inputFiles,
            additionalJars,
            outputFileEncoding,
            cmdLine);
    if (TestsuiteUtils.verbose) {
      System.out.println("Running scanner on [" + current.getName() + "]");
    }

    // check for output conformance
    File expected = new File(current.getName() + ".output");

    if (expected.exists()) {
      DiffStream check = new DiffStream();
      String diff;
      try (InputStreamReader expectedContent =
          new InputStreamReader(
              Files.newInputStream(Paths.get(expected.toString())), outputFileEncoding)) {
        diff =
            check.diff(jflexDiff, new StringReader(classExecResult.getOutput()), expectedContent);
      } catch (IOException e) {
        System.out.println("Error opening file " + expected);
        throw new TestFailException();
      }
      if (diff != null) {
        System.out.println("Test failed, unexpected output: " + diff);
        System.out.println("Test output: " + classExecResult.getOutput());
        throw new TestFailException();
      }
    } else {
      System.out.println("Warning: no file for expected output [" + expected + "]");
    }

    // System.out.println(classExecResult);
  }

  @Override
  public String toString() {
    return "Test name: "
        + testName
        + "\nDescription: "
        + description
        + "JFlexFail: "
        + expectJFlexFail
        + " JavacFail: "
        + expectJavacFail
        + "\n"
        + "JFlex Command line: "
        + jflexCmdln
        + (null != javacFiles ? " Javac Files: " + Arrays.toString(javacFiles.toArray()) : "")
        + "\n"
        + "Files to run Tester on "
        + inputOutput
        + (null != commonInputFile ? " Common input file: " + commonInputFile : "")
        + "Java version "
        + javaVersion;
  }
}
