package tech.picnic.errorprone.bugpatterns;

import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
import com.google.errorprone.CompilationTestHelper;
import org.junit.jupiter.api.Test;

final class EagerStringFormattingTest {
  @Test
  void identification() {
    CompilationTestHelper.newInstance(EagerStringFormatting.class, getClass())
        .expectErrorMessage("DEFER", m -> m.contains("String formatting can be deferred\n"))
        .expectErrorMessage(
            "DEFER_EXTRA_VARIABLE",
            m ->
                m.contains(
                    "String formatting can be deferred (but this requires introducing an effectively final variable)"))
        .expectErrorMessage(
            "DEFER_SIMPLIFIED_GUAVA",
            m ->
                m.contains(
                    "String formatting can be deferred (assuming that Guava's simplified formatting support suffices)"))
        .expectErrorMessage(
            "DEFER_SIMPLIFIED_SLF4J",
            m ->
                m.contains(
                    "String formatting can be deferred (assuming that SLF4J's simplified formatting support suffices)"))
        .expectErrorMessage(
            "VACUOUS", m -> m.contains("String formatting never yields `null` expression"))
        .addSourceLines(
            "A.java",
            "import static com.google.common.base.Preconditions.checkArgument;",
            "import static com.google.common.base.Preconditions.checkNotNull;",
            "import static com.google.common.base.Preconditions.checkState;",
            "import static com.google.common.base.Verify.verify;",
            "import static com.google.common.base.Verify.verifyNotNull;",
            "import static java.util.Objects.requireNonNull;",
            "",
            "import java.util.Formattable;",
            "import java.util.Locale;",
            "import org.slf4j.Logger;",
            "import org.slf4j.LoggerFactory;",
            "import org.slf4j.Marker;",
            "",
            "class A {",
            "  private static final Logger LOG = LoggerFactory.getLogger(A.class);",
            "",
            "  private int nonFinalField = 0;",
            "",
            "  void m() {",
            "    Formattable formattable = (formatter, flags, width, precision) -> {};",
            "    int effectivelyFinalLocal = 0;",
            "    /* A local variable that is also not effectively final. */",
            "    int nonFinalLocal = 0;",
            "    nonFinalLocal = 1;",
            "",
            "    String.format(\"%s\", \"foo\");",
            "    String.format(Locale.US, \"%s\", \"foo\");",
            "    \"%s\".formatted(\"foo\");",
            "    String.format(\"%s\", \"foo\", \"bar\");",
            "    String.format(\"%s %s\", \"foo\", \"bar\");",
            "    String.format(\"%s %s %%\", \"foo\", \"bar\");",
            "",
            "    System.out.println(String.format(\"%s\", nonFinalLocal));",
            "",
            "    requireNonNull(\"never-null\");",
            "    requireNonNull(\"never-null\", () -> String.format(\"Format string: %s\", nonFinalField));",
            "    // BUG: Diagnostic matches: VACUOUS",
            "    requireNonNull(String.format(\"Never-null format string: %s\", nonFinalField));",
            "    // BUG: Diagnostic matches: VACUOUS",
            "    requireNonNull(\"Never-null format string: %s\".formatted(nonFinalField), \"message\");",
            "    // BUG: Diagnostic matches: VACUOUS",
            "    requireNonNull(",
            "        String.format(\"Never-null format string\"), String.format(\"Malformed format string: %\"));",
            "    // BUG: Diagnostic matches: DEFER_EXTRA_VARIABLE",
            "    requireNonNull(\"never-null\", String.format(\"Format string: %s\", nonFinalLocal));",
            "    // BUG: Diagnostic matches: DEFER",
            "    requireNonNull(\"never-null\", String.format(\"Format string: %s\", effectivelyFinalLocal));",
            "    // BUG: Diagnostic matches: DEFER",
            "    requireNonNull(",
            "        \"never-null\",",
            "        String.format(",
            "            \"Custom format string: %s, %d, %s\", getClass(), nonFinalField, \"string-constant\"));",
            "",
            "    checkArgument(true);",
            "    checkNotNull(\"never-null\");",
            "    checkState(false);",
            "    verify(true);",
            "    verifyNotNull(\"never-null\");",
            "    checkArgument(false, \"Without format string\");",
            "    checkNotNull(\"never-null\", \"Without format string\");",
            "    checkState(true, \"Without format string\");",
            "    verify(false, \"Without format string\");",
            "    verifyNotNull(\"never-null\", \"Without format string\");",
            "    checkArgument(true, \"With format string: %s\", nonFinalLocal);",
            "    checkNotNull(\"never-null\", \"With format string: %s\", nonFinalLocal);",
            "    checkState(false, \"With format string: %s\", nonFinalLocal);",
            "    verify(true, \"With format string: %s\", nonFinalLocal);",
            "    verifyNotNull(\"never-null\", \"With format string: %s\", nonFinalLocal);",
            "    // BUG: Diagnostic matches: VACUOUS",
            "    checkNotNull(String.format(\"Never-null format string: %s\", nonFinalLocal));",
            "    // BUG: Diagnostic matches: VACUOUS",
            "    verifyNotNull(\"Never-null format string: %s\".formatted(nonFinalLocal), \"message\");",
            "    // BUG: Diagnostic matches: VACUOUS",
            "    checkNotNull(",
            "        String.format(\"Never-null format string\"), String.format(\"Malformed format string: %\"));",
            "    // BUG: Diagnostic matches: DEFER_SIMPLIFIED_GUAVA",
            "    checkArgument(true, String.format(toString()));",
            "    // BUG: Diagnostic matches: DEFER_SIMPLIFIED_GUAVA",
            "    checkNotNull(\"never-null\", toString().formatted());",
            "    // BUG: Diagnostic matches: DEFER_SIMPLIFIED_GUAVA",
            "    checkState(true, String.format(\"Custom format string: %d\", nonFinalLocal));",
            "    // BUG: Diagnostic matches: DEFER_SIMPLIFIED_GUAVA",
            "    verify(true, \"Mismatched format string:\".formatted(nonFinalLocal));",
            "    // BUG: Diagnostic matches: DEFER_SIMPLIFIED_GUAVA",
            "    verifyNotNull(\"never-null\", \"Mismatched format string: %d\".formatted());",
            "    // BUG: Diagnostic matches: DEFER_SIMPLIFIED_GUAVA",
            "    checkArgument(true, String.format(\"Malformed format string: %\"));",
            "    // BUG: Diagnostic matches: DEFER_SIMPLIFIED_GUAVA",
            "    checkNotNull(\"never-null\", \"Format string with `Formattable`: %s\".formatted(formattable));",
            "    // BUG: Diagnostic matches: DEFER_SIMPLIFIED_GUAVA",
            "    checkState(true, String.format(\"Generated format string: %%s\"), nonFinalLocal);",
            "    // BUG: Diagnostic matches: DEFER_SIMPLIFIED_GUAVA",
            "    verify(",
            "        true,",
            "        \"Format string with format string argument: %s\",",
            "        String.format(\"Format string argument: %s\", nonFinalLocal));",
            "    // BUG: Diagnostic matches: DEFER",
            "    verifyNotNull(",
            "        \"never-null\", String.format(\"Format string: %s, %s\", nonFinalLocal, nonFinalLocal));",
            "    // BUG: Diagnostic matches: DEFER",
            "    checkArgument(true, \"Format string: %s%%\".formatted(nonFinalLocal));",
            "    // BUG: Diagnostic matches: DEFER",
            "    checkNotNull(",
            "        \"never-null\", String.format(Locale.US, \"Format string with locale: %s\", nonFinalLocal));",
            "",
            "    LOG.trace(\"Without format string\");",
            "    LOG.debug(\"With format string: {}\", nonFinalLocal);",
            "    LOG.info((Marker) null, \"With marker\");",
            "    LOG.warn((Marker) null, \"With marker and format string: {}\", nonFinalLocal);",
            "    LOG.error(\"With throwable\", new RuntimeException());",
            "    LOG.trace(\"With throwable and format string: {}\", nonFinalLocal, new RuntimeException());",
            "    LOG.debug((Marker) null, \"With marker and throwable\", new RuntimeException());",
            "    LOG.info(",
            "        (Marker) null,",
            "        \"With marker, throwable and format string: {}\",",
            "        nonFinalLocal,",
            "        new RuntimeException());",
            "    // BUG: Diagnostic matches: DEFER_SIMPLIFIED_SLF4J",
            "    LOG.warn(String.format(toString()));",
            "    // BUG: Diagnostic matches: DEFER_SIMPLIFIED_SLF4J",
            "    LOG.error(toString().formatted());",
            "    // BUG: Diagnostic matches: DEFER_SIMPLIFIED_SLF4J",
            "    LOG.trace(String.format(\"Custom format string: %d\", nonFinalLocal));",
            "    // BUG: Diagnostic matches: DEFER_SIMPLIFIED_SLF4J",
            "    LOG.debug(\"Mismatched format string:\".formatted(nonFinalLocal));",
            "    // BUG: Diagnostic matches: DEFER_SIMPLIFIED_SLF4J",
            "    LOG.info(\"Mismatched format string %d:\".formatted());",
            "    // BUG: Diagnostic matches: DEFER_SIMPLIFIED_SLF4J",
            "    LOG.warn(String.format(\"Malformed format string: %\"));",
            "    // BUG: Diagnostic matches: DEFER_SIMPLIFIED_SLF4J",
            "    LOG.error(\"Format string with `Formattable`: %s\".formatted(formattable));",
            "    // BUG: Diagnostic matches: DEFER_SIMPLIFIED_SLF4J",
            "    LOG.trace(String.format(\"Generated format string: {}\"), nonFinalLocal);",
            "    // BUG: Diagnostic matches: DEFER_SIMPLIFIED_SLF4J",
            "    LOG.debug(",
            "        \"Format string with format string argument: {}\",",
            "        String.format(\"Format string argument: %s\", nonFinalLocal));",
            "    // BUG: Diagnostic matches: DEFER",
            "    LOG.info(String.format(\"Vacuous format string %%\"));",
            "    // BUG: Diagnostic matches: DEFER",
            "    LOG.warn(String.format(\"With format string: %s, %s\", nonFinalLocal, nonFinalLocal));",
            "    // BUG: Diagnostic matches: DEFER",
            "    LOG.error(String.format(Locale.ROOT, \"With vacuous localized format string %%\"));",
            "    // BUG: Diagnostic matches: DEFER",
            "    LOG.trace((Marker) null, String.format(\"With marker and format string: %s\", nonFinalLocal));",
            "    // BUG: Diagnostic matches: DEFER",
            "    LOG.debug(",
            "        String.format(\"With throwable and format string: %s\", nonFinalLocal),",
            "        new RuntimeException());",
            "    // BUG: Diagnostic matches: DEFER",
            "    LOG.info(",
            "        (Marker) null,",
            "        String.format(\"With marker, throwable and format string: %s\", nonFinalLocal),",
            "        new RuntimeException());",
            "  }",
            "}")
        .doTest();
  }

  @Test
  void replacement() {
    BugCheckerRefactoringTestHelper.newInstance(EagerStringFormatting.class, getClass())
        .addInputLines(
            "A.java",
            "import static com.google.common.base.Preconditions.checkArgument;",
            "import static com.google.common.base.Preconditions.checkNotNull;",
            "import static com.google.common.base.Preconditions.checkState;",
            "import static com.google.common.base.Verify.verify;",
            "import static com.google.common.base.Verify.verifyNotNull;",
            "import static java.util.Objects.requireNonNull;",
            "",
            "import java.util.Locale;",
            "import org.slf4j.Logger;",
            "import org.slf4j.LoggerFactory;",
            "import org.slf4j.Marker;",
            "",
            "class A {",
            "  private static final Logger LOG = LoggerFactory.getLogger(A.class);",
            "  private static final String GUAVA_COMPATIBLE_PATTERN = \"with-only-%s-placeholder\";",
            "  private static final String GUAVA_INCOMPATIBLE_PATTERN = \"with-%%-marker\";",
            "",
            "  void m() {",
            "    requireNonNull(\"never-null\", String.format(\"Format string: %s\", 0));",
            "",
            "    checkArgument(true, String.format(\"Vacuous format string %%\"));",
            "    checkNotNull(\"never-null\", \"Format string: %s %s%%\".formatted(1, 2));",
            "    checkState(false, String.format(Locale.US, \"Format string with locale: %s\", 3));",
            "    verify(true, GUAVA_COMPATIBLE_PATTERN.formatted(4));",
            "    verifyNotNull(\"never-null\", String.format(Locale.ENGLISH, GUAVA_COMPATIBLE_PATTERN, 5));",
            "    checkArgument(false, GUAVA_INCOMPATIBLE_PATTERN.formatted());",
            "    checkNotNull(\"never-null\", String.format(GUAVA_INCOMPATIBLE_PATTERN));",
            "",
            "    LOG.trace(\"Vacuous format string %%\".formatted());",
            "    LOG.debug(String.format(\"With format string: %s, %s%%\", 6, 7));",
            "    LOG.info(String.format(Locale.ROOT, \"With vacuous localized format string %%\"));",
            "    LOG.warn((Marker) null, \"With marker and format string: %s\".formatted(8));",
            "    LOG.error(",
            "        String.format(Locale.US, \"With throwable and format string: %s, %s\", 9, 10),",
            "        new RuntimeException());",
            "    LOG.trace(",
            "        (Marker) null,",
            "        \"With marker, throwable and format string: %s\".formatted(11),",
            "        new RuntimeException());",
            "    LOG.debug(GUAVA_COMPATIBLE_PATTERN.formatted(12));",
            "    LOG.info(String.format(Locale.ENGLISH, GUAVA_COMPATIBLE_PATTERN, 13));",
            "    LOG.warn(GUAVA_INCOMPATIBLE_PATTERN.formatted());",
            "    LOG.error(String.format(GUAVA_INCOMPATIBLE_PATTERN));",
            "  }",
            "}")
        .addOutputLines(
            "A.java",
            "import static com.google.common.base.Preconditions.checkArgument;",
            "import static com.google.common.base.Preconditions.checkNotNull;",
            "import static com.google.common.base.Preconditions.checkState;",
            "import static com.google.common.base.Verify.verify;",
            "import static com.google.common.base.Verify.verifyNotNull;",
            "import static java.util.Objects.requireNonNull;",
            "",
            "import java.util.Locale;",
            "import org.slf4j.Logger;",
            "import org.slf4j.LoggerFactory;",
            "import org.slf4j.Marker;",
            "",
            "class A {",
            "  private static final Logger LOG = LoggerFactory.getLogger(A.class);",
            "  private static final String GUAVA_COMPATIBLE_PATTERN = \"with-only-%s-placeholder\";",
            "  private static final String GUAVA_INCOMPATIBLE_PATTERN = \"with-%%-marker\";",
            "",
            "  void m() {",
            "    requireNonNull(\"never-null\", () -> String.format(\"Format string: %s\", 0));",
            "",
            "    checkArgument(true, \"Vacuous format string %\");",
            "    checkNotNull(\"never-null\", \"Format string: %s %s%\", 1, 2);",
            "    checkState(false, \"Format string with locale: %s\", 3);",
            "    verify(true, GUAVA_COMPATIBLE_PATTERN, 4);",
            "    verifyNotNull(\"never-null\", GUAVA_COMPATIBLE_PATTERN, 5);",
            "    checkArgument(false, \"with-%-marker\");",
            "    checkNotNull(\"never-null\", \"with-%-marker\");",
            "",
            "    LOG.trace(\"Vacuous format string %\");",
            "    LOG.debug(\"With format string: {}, {}%\", 6, 7);",
            "    LOG.info(\"With vacuous localized format string %\");",
            "    LOG.warn((Marker) null, \"With marker and format string: {}\", 8);",
            "    LOG.error(\"With throwable and format string: {}, {}\", 9, 10, new RuntimeException());",
            "    LOG.trace(",
            "        (Marker) null, \"With marker, throwable and format string: {}\", 11, new RuntimeException());",
            "    LOG.debug(\"with-only-{}-placeholder\", 12);",
            "    LOG.info(\"with-only-{}-placeholder\", 13);",
            "    LOG.warn(\"with-%-marker\");",
            "    LOG.error(\"with-%-marker\");",
            "  }",
            "}")
        .doTest(TestMode.TEXT_MATCH);
  }
}
