/*
 * Copyright 2015 Google Inc.
 *
 * 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 com.google.googlejavaformat.java;

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

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteStreams;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.ProcessBuilder.Redirect;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.util.EnumSet;
import java.util.Locale;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for {@link Main}. */
@RunWith(JUnit4.class)
public class MainTest {

  @Rule public TemporaryFolder testFolder = new TemporaryFolder();

  // PrintWriter instances used below are hard-coded to use system-default line separator.
  private final Joiner joiner = Joiner.on(System.lineSeparator());

  @Test
  public void testUsageOutput() {
    StringWriter out = new StringWriter();
    StringWriter err = new StringWriter();
    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);

    try {
      main.format("--help");
      throw new AssertionError("Expected UsageException to be thrown");
    } catch (UsageException e) {

      String usage = e.getMessage();

      // Check that doc links are included.
      assertThat(usage).contains("https://github.com/google/google-java-format");
      assertThat(usage).contains("Usage: google-java-format");

      // Sanity check that a flag and description is in included.
      assertThat(usage).contains("--length");
      assertThat(usage).contains("Character length to format.");

      // Check that some of the additional text is included.
      assertThat(usage).contains("the result is sent to stdout");
    }
  }

  @Test
  public void version() throws UsageException {
    StringWriter out = new StringWriter();
    StringWriter err = new StringWriter();
    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
    assertThat(main.format("-version")).isEqualTo(0);
    assertThat(err.toString()).contains("google-java-format: Version ");
  }

  @Test
  public void preserveOriginalFile() throws Exception {
    Path path = testFolder.newFile("Test.java").toPath();
    Files.write(path, "class Test {}\n".getBytes(UTF_8));
    try {
      Files.setPosixFilePermissions(path, EnumSet.of(PosixFilePermission.OWNER_READ));
    } catch (UnsupportedOperationException e) {
      return;
    }
    Main main =
        new Main(
            new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8)), true),
            new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.err, UTF_8)), true),
            System.in);
    int errorCode = main.format("-replace", path.toAbsolutePath().toString());
    assertWithMessage("Error Code").that(errorCode).isEqualTo(0);
  }

  @Test
  public void testMain() throws Exception {
    Process process =
        new ProcessBuilder(
                ImmutableList.of(
                    Paths.get(System.getProperty("java.home")).resolve("bin/java").toString(),
                    "-cp",
                    System.getProperty("java.class.path"),
                    Main.class.getName()))
            .redirectError(Redirect.PIPE)
            .redirectOutput(Redirect.PIPE)
            .start();
    process.waitFor();
    String err = new String(ByteStreams.toByteArray(process.getErrorStream()), UTF_8);
    assertThat(err).contains("Usage: google-java-format");
    assertThat(process.exitValue()).isEqualTo(0);
  }

  // end to end javadoc formatting test
  @Test
  public void javadoc() throws Exception {
    String[] input = {
      "/**",
      " * graph",
      " *",
      " * graph",
      " *",
      " * @param foo lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
          + " eiusmod tempor incididunt ut labore et dolore magna aliqua",
      " */",
      "class Test {",
      "  /**",
      "   * creates entropy",
      "   */",
      "  public static void main(String... args) {}",
      "}",
    };
    String[] expected = {
      "/**",
      " * graph",
      " *",
      " * <p>graph",
      " *",
      " * @param foo lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
          + " eiusmod tempor",
      " *     incididunt ut labore et dolore magna aliqua",
      " */",
      "class Test {",
      "  /** creates entropy */",
      "  public static void main(String... args) {}",
      "}",
      "",
    };
    InputStream in = new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8));
    StringWriter out = new StringWriter();
    Main main =
        new Main(
            new PrintWriter(out, true),
            new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.err, UTF_8)), true),
            in);
    assertThat(main.format("-")).isEqualTo(0);
    assertThat(out.toString()).isEqualTo(joiner.join(expected));
  }

  // end to end import fixing test
  @Test
  public void imports() throws Exception {
    String[] input = {
      "import java.util.LinkedList;",
      "import java.util.List;",
      "import java.util.ArrayList;",
      "class Test {",
      "  /**",
      "   * May be an {@link ArrayList}.",
      "   */",
      "  public static List<String> names;",
      "}",
    };
    String[] expected = {
      "import java.util.ArrayList;",
      "import java.util.List;",
      "",
      "class Test {",
      "  /**",
      "   * May be an {@link ArrayList}.",
      "   */",
      "  public static List<String> names;",
      "}",
    };
    InputStream in = new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8));
    StringWriter out = new StringWriter();
    Main main =
        new Main(
            new PrintWriter(out, true),
            new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.err, UTF_8)), true),
            in);
    assertThat(main.format("-", "--fix-imports-only")).isEqualTo(0);
    assertThat(out.toString()).isEqualTo(joiner.join(expected));
  }

  @Test
  public void optimizeImportsDoesNotLeaveEmptyLines() throws Exception {
    String[] input = {
      "package abc;",
      "",
      "import java.util.LinkedList;",
      "import java.util.List;",
      "import java.util.ArrayList;",
      "",
      "import static java.nio.charset.StandardCharsets.UTF_8;",
      "",
      "import java.util.EnumSet;",
      "",
      "class Test ",
      "extends ArrayList {",
      "}"
    };
    String[] expected = {
      "package abc;", //
      "",
      "import java.util.ArrayList;",
      "",
      "class Test extends ArrayList {}",
      ""
    };

    // pre-check expectation with local formatter instance
    String optimized = new Formatter().formatSourceAndFixImports(joiner.join(input));
    assertThat(optimized).isEqualTo(joiner.join(expected));

    InputStream in = new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8));
    StringWriter out = new StringWriter();
    Main main =
        new Main(
            new PrintWriter(out, true),
            new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.err, UTF_8)), true),
            in);
    assertThat(main.format("-")).isEqualTo(0);
    assertThat(out.toString()).isEqualTo(joiner.join(expected));
  }

  // test that -lines handling works with import removal
  @Test
  public void importRemovalLines() throws Exception {
    String[] input = {
      "import java.util.ArrayList;",
      "import java.util.List;",
      "class Test {",
      "ArrayList<String> a = new ArrayList<>();",
      "ArrayList<String> b = new ArrayList<>();",
      "}",
    };
    String[] expected = {
      "import java.util.ArrayList;",
      "",
      "class Test {",
      "  ArrayList<String> a = new ArrayList<>();",
      "ArrayList<String> b = new ArrayList<>();",
      "}",
    };
    StringWriter out = new StringWriter();
    Main main =
        new Main(
            new PrintWriter(out, true),
            new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.err, UTF_8)), true),
            new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8)));
    assertThat(main.format("-", "-lines", "4")).isEqualTo(0);
    assertThat(out.toString()).isEqualTo(joiner.join(expected));
  }

  // test that errors are reported on the right line when imports are removed
  @Test
  public void importRemoveErrorParseError() throws Exception {
    Locale backupLocale = Locale.getDefault();
    try {
      Locale.setDefault(Locale.ROOT);

      String[] input = {
        "import java.util.ArrayList;", //
        "import java.util.List;",
        "class Test {",
        "}}",
      };
      StringWriter out = new StringWriter();
      StringWriter err = new StringWriter();
      Main main =
          new Main(
              new PrintWriter(out, true),
              new PrintWriter(err, true),
              new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8)));
      assertThat(main.format("-")).isEqualTo(1);
      assertThat(err.toString()).contains("<stdin>:4:3: error: class, interface");

    } finally {
      Locale.setDefault(backupLocale);
    }
  }

  @Test
  public void packageInfo() throws Exception {
    String[] input = {
      "@CheckReturnValue",
      "@ParametersAreNonnullByDefault",
      "package com.google.common.labs.base;",
      "",
      "import javax.annotation.CheckReturnValue;",
      "import javax.annotation.ParametersAreNonnullByDefault;",
      "",
    };
    StringWriter out = new StringWriter();
    StringWriter err = new StringWriter();
    Main main =
        new Main(
            new PrintWriter(out, true),
            new PrintWriter(err, true),
            new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8)));
    assertThat(main.format("-")).isEqualTo(0);
    assertThat(out.toString()).isEqualTo(joiner.join(input));
  }

  @Test
  public void newline() throws Exception {
    StringWriter out = new StringWriter();
    StringWriter err = new StringWriter();
    Main main =
        new Main(
            new PrintWriter(out, true),
            new PrintWriter(err, true),
            new ByteArrayInputStream("class T {}\n\t".getBytes(UTF_8)));
    assertThat(main.format("-")).isEqualTo(0);
    assertThat(out.toString()).isEqualTo("class T {}\n");
  }

  @Test
  public void dryRunStdinUnchanged() throws Exception {
    StringWriter out = new StringWriter();
    StringWriter err = new StringWriter();
    Main main =
        new Main(
            new PrintWriter(out, true),
            new PrintWriter(err, true),
            new ByteArrayInputStream("class Test {}\n".getBytes(UTF_8)));
    assertThat(main.format("-n", "-")).isEqualTo(0);
    assertThat(out.toString()).isEmpty();
    assertThat(err.toString()).isEmpty();
  }

  @Test
  public void dryRunStdinChanged() throws Exception {
    StringWriter out = new StringWriter();
    StringWriter err = new StringWriter();
    String input = "class Test {\n}\n";
    Main main =
        new Main(
            new PrintWriter(out, true),
            new PrintWriter(err, true),
            new ByteArrayInputStream(input.getBytes(UTF_8)));
    assertThat(main.format("-n", "-")).isEqualTo(0);
    assertThat(out.toString()).isEqualTo("<stdin>" + System.lineSeparator());
    assertThat(err.toString()).isEmpty();
  }

  @Test
  public void dryRunFiles() throws Exception {
    Path a = testFolder.newFile("A.java").toPath();
    Path b = testFolder.newFile("B.java").toPath();
    Path c = testFolder.newFile("C.java").toPath();
    Files.write(a, "class A {}\n".getBytes(UTF_8));
    Files.write(b, "class B {\n}\n".getBytes(UTF_8));
    Files.write(c, "class C {\n}\n".getBytes(UTF_8));

    StringWriter out = new StringWriter();
    StringWriter err = new StringWriter();
    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
    int exitCode =
        main.format(
            "-n",
            a.toAbsolutePath().toAbsolutePath().toString(),
            b.toAbsolutePath().toString(),
            c.toAbsolutePath().toString());

    assertThat(exitCode).isEqualTo(0);

    assertThat(out.toString())
        .isEqualTo(
            b.toAbsolutePath().toString()
                + System.lineSeparator()
                + c.toAbsolutePath().toString()
                + System.lineSeparator());
    assertThat(err.toString()).isEmpty();
  }

  @Test
  public void keepGoingWhenFilesDontExist() throws Exception {
    Path a = testFolder.newFile("A.java").toPath();
    Path b = testFolder.newFile("B.java").toPath();
    File cFile = testFolder.newFile("C.java");
    Path c = cFile.toPath();
    cFile.delete();

    Files.write(a, "class A{}\n".getBytes(UTF_8));
    Files.write(b, "class B{}\n".getBytes(UTF_8));

    StringWriter out = new StringWriter();
    StringWriter err = new StringWriter();
    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);

    int exitCode =
        main.format(
            "",
            a.toAbsolutePath().toString(),
            c.toAbsolutePath().toString(),
            b.toAbsolutePath().toString());

    // Formatter returns failure if a file was not present.
    assertThat(exitCode).isEqualTo(1);

    // Present files were correctly formatted.
    assertThat(out.toString()).isEqualTo("class A {}\nclass B {}\n");

    // File not found still showed error.
    assertThat(err.toString()).isNotEmpty();
  }

  @Test
  public void exitIfChangedStdin() throws Exception {
    Path path = testFolder.newFile("Test.java").toPath();
    Files.write(path, "class Test {\n}\n".getBytes(UTF_8));
    Process process =
        new ProcessBuilder(
                ImmutableList.of(
                    Paths.get(System.getProperty("java.home")).resolve("bin/java").toString(),
                    "-cp",
                    System.getProperty("java.class.path"),
                    Main.class.getName(),
                    "-n",
                    "--set-exit-if-changed",
                    "-"))
            .redirectInput(path.toFile())
            .redirectError(Redirect.PIPE)
            .redirectOutput(Redirect.PIPE)
            .start();
    process.waitFor();
    String err = new String(ByteStreams.toByteArray(process.getErrorStream()), UTF_8);
    String out = new String(ByteStreams.toByteArray(process.getInputStream()), UTF_8);
    assertThat(err).isEmpty();
    assertThat(out).isEqualTo("<stdin>" + System.lineSeparator());
    assertThat(process.exitValue()).isEqualTo(1);
  }

  @Test
  public void exitIfChangedFiles() throws Exception {
    Path path = testFolder.newFile("Test.java").toPath();
    Files.write(path, "class Test {\n}\n".getBytes(UTF_8));
    Process process =
        new ProcessBuilder(
                ImmutableList.of(
                    Paths.get(System.getProperty("java.home")).resolve("bin/java").toString(),
                    "-cp",
                    System.getProperty("java.class.path"),
                    Main.class.getName(),
                    "-n",
                    "--set-exit-if-changed",
                    path.toAbsolutePath().toString()))
            .redirectError(Redirect.PIPE)
            .redirectOutput(Redirect.PIPE)
            .start();
    process.waitFor();
    String err = new String(ByteStreams.toByteArray(process.getErrorStream()), UTF_8);
    String out = new String(ByteStreams.toByteArray(process.getInputStream()), UTF_8);
    assertThat(err).isEmpty();
    assertThat(out).isEqualTo(path.toAbsolutePath().toString() + System.lineSeparator());
    assertThat(process.exitValue()).isEqualTo(1);
  }

  @Test
  public void assumeFilename_error() throws Exception {
    String[] input = {
      "class Test {}}",
    };
    StringWriter out = new StringWriter();
    StringWriter err = new StringWriter();
    Main main =
        new Main(
            new PrintWriter(out, true),
            new PrintWriter(err, true),
            new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8)));
    assertThat(main.format("--assume-filename=Foo.java", "-")).isEqualTo(1);
    assertThat(err.toString()).contains("Foo.java:1:15: error: class, interface");
  }

  @Test
  public void assumeFilename_dryRun() throws Exception {
    String[] input = {
      "class Test {", //
      "}",
    };
    StringWriter out = new StringWriter();
    StringWriter err = new StringWriter();
    Main main =
        new Main(
            new PrintWriter(out, true),
            new PrintWriter(err, true),
            new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8)));
    assertThat(main.format("--dry-run", "--assume-filename=Foo.java", "-")).isEqualTo(0);
    assertThat(out.toString()).isEqualTo("Foo.java" + System.lineSeparator());
  }

  @Test
  public void reflowLongStrings() throws Exception {
    String[] input = {
      "class T {", //
      "  String s = \"one long incredibly unbroken sentence moving from topic to topic so that no"
          + " one had a chance to interrupt\";",
      "}"
    };
    String[] expected = {
      "class T {",
      "  String s =",
      "      \"one long incredibly unbroken sentence moving from topic to topic so that no one had"
          + " a\"",
      "          + \" chance to interrupt\";",
      "}",
      "",
    };
    InputStream in = new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8));
    StringWriter out = new StringWriter();
    Main main =
        new Main(
            new PrintWriter(out, true),
            new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.err, UTF_8)), true),
            in);
    assertThat(main.format("-")).isEqualTo(0);
    assertThat(out.toString()).isEqualTo(joiner.join(expected));
  }

  @Test
  public void noReflowLongStrings() throws Exception {
    String[] input = {
      "class T {", //
      "  String s = \"one long incredibly unbroken sentence moving from topic to topic so that no"
          + " one had a chance to interrupt\";",
      "}"
    };
    String[] expected = {
      "class T {",
      "  String s =",
      "      \"one long incredibly unbroken sentence moving from topic to topic so that no one had"
          + " a chance to interrupt\";",
      "}",
      "",
    };
    InputStream in = new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8));
    StringWriter out = new StringWriter();
    Main main =
        new Main(
            new PrintWriter(out, true),
            new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.err, UTF_8)), true),
            in);
    assertThat(main.format("--skip-reflowing-long-strings", "-")).isEqualTo(0);
    assertThat(out.toString()).isEqualTo(joiner.join(expected));
  }

  @Test
  public void noFormatJavadoc() throws Exception {
    String[] input = {
      "/**",
      " * graph",
      " *",
      " * graph",
      " *",
      " * @param foo lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
          + " eiusmod tempor incididunt ut labore et dolore magna aliqua",
      " */",
      "class Test {",
      "  /**",
      "   * creates entropy",
      "   */",
      "  public static void main(String... args) {}",
      "}",
      "",
    };
    InputStream in = new ByteArrayInputStream(joiner.join(input).getBytes(UTF_8));
    StringWriter out = new StringWriter();
    Main main =
        new Main(
            new PrintWriter(out, true),
            new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.err, UTF_8)), true),
            in);
    assertThat(main.format("--skip-javadoc-formatting", "-")).isEqualTo(0);
    assertThat(out.toString()).isEqualTo(joiner.join(input));
  }
}